Micron Document
🎖️GitЯра🎖️

Commit f5128798a808219a28e9ee0916c079edcb203744


Parents : 72436e7
Author : James Rich <2199651+jamesarich@users.noreply.github.com>
Signature : Signature validation error
Date : 2026-05-18T20:10:47-05:00
Committer : GitHub <noreply@github.com>
Date : 2026-05-19T01:10:47Z

feat: adopt Material 3 Expressive design system (M3-native APIs only) (#5479)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Changes

161 files changed, 1864 insertions(+), 3053 deletions(-)

M AGENTS.md +1 -1

Diff

diff --git a/.github/agents/speckit.verify.run.agent.md b/.github/agents/speckit.verify.run.agent.md
index 2ea0c3a856..dfd2e9f9f8 100644
--- a/.github/agents/speckit.verify.run.agent.md
+++ b/.github/agents/speckit.verify.run.agent.md
@@ -34,7 +34,7 @@ Validate the implementation against its specification artifacts (`spec.md`, `pla
Run `.specify/scripts/bash/check-prerequisites.sh --json --paths-only` from repo root.
1. **Script succeeds** (on a feature branch): Parse JSON for FEATURE_DIR. Set `FEATURE_BRANCH = true`. Proceed to next step.
-2. **Script fails** (not on a feature branch): You MUST prompt for available features (Scan `specs/NNN-*/` to get available features). Use the **AskUserQuestion tool** to let the user select. **Do NOT guess or auto-select a change. Always let the user choose.**
+2. **Script fails** (not on a feature branch): You MUST prompt for available features (Scan `specs/*/` to get available features). Use the **AskUserQuestion tool** to let the user select. **Do NOT guess or auto-select a change. Always let the user choose.**
Derive absolute paths:

diff --git a/.skills/speckit/SKILL.md b/.skills/speckit/SKILL.md
index d546d9be6a..badec3fbff 100644
--- a/.skills/speckit/SKILL.md
+++ b/.skills/speckit/SKILL.md
@@ -52,7 +52,7 @@ The standard SDD cycle for a new feature:
```text
1. /speckit.specify "Feature description here"
- → Creates specs/<NNN>-feature-name/spec.md
+ → Creates specs/<YYYYMMDD-HHMMSS>-feature-name/spec.md
→ Auto-creates feature branch via git hook
2. /speckit.clarify
@@ -85,11 +85,11 @@ This runs: specify → (review gate) → plan → (review gate) → tasks → im
## File Structure
-Spec Kit produces files under `specs/<NNN>-feature-name/`:
+Spec Kit produces files under `specs/<YYYYMMDD-HHMMSS>-feature-name/`:
```
specs/
-└── 001-feature-name/
+└── 20260513-160000-feature-name/
├── spec.md # Feature specification (FRs, NFRs, SCs, user stories)
├── plan.md # Implementation plan (architecture, phases)
├── tasks.md # Dependency-ordered task list

diff --git a/.specify/extensions/verify/commands/verify.md b/.specify/extensions/verify/commands/verify.md
index bf43c326ad..d14bf27b46 100644
--- a/.specify/extensions/verify/commands/verify.md
+++ b/.specify/extensions/verify/commands/verify.md
@@ -30,7 +30,7 @@ Validate the implementation against its specification artifacts (`spec.md`, `pla
Run `{SCRIPT}` from repo root.
1. **Script succeeds** (on a feature branch): Parse JSON for FEATURE_DIR. Set `FEATURE_BRANCH = true`. Proceed to next step.
-2. **Script fails** (not on a feature branch): You MUST prompt for available features (Scan `specs/NNN-*/` to get available features). Use the **AskUserQuestion tool** to let the user select. **Do NOT guess or auto-select a change. Always let the user choose.**
+2. **Script fails** (not on a feature branch): You MUST prompt for available features (Scan `specs/*/` to get available features). Use the **AskUserQuestion tool** to let the user select. **Do NOT guess or auto-select a change. Always let the user choose.**
Derive absolute paths:

diff --git a/.specify/feature.json b/.specify/feature.json
index c800a63214..b5d645344b 100644
--- a/.specify/feature.json
+++ b/.specify/feature.json
@@ -1 +1 @@
-{"feature_directory":"specs/006-kmp-project-structure"}
+{"feature_directory":"specs/20260513-160000-m3-expressive-adoption"}

diff --git a/.specify/init-options.json b/.specify/init-options.json
index e4fce1fc29..3d0c55bc7d 100644
--- a/.specify/init-options.json
+++ b/.specify/init-options.json
@@ -1,6 +1,6 @@
{
"ai": "opencode",
- "branch_numbering": "sequential",
+ "branch_numbering": "timestamp",
"context_file": "AGENTS.md",
"here": true,
"integration": "opencode",

diff --git a/AGENTS.md b/AGENTS.md
index f7d3bdcd63..83dd3aca20 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -49,5 +49,5 @@ You are an expert Android/KMP engineer. Maintain architectural boundaries, use M
<!-- SPECKIT START -->
For additional context about technologies to be used, project structure,
shell commands, and other important information, read the current plan
-at `specs/006-kmp-project-structure/plan.md`
+at `specs/20260513-160000-m3-expressive-adoption/plan.md`
<!-- SPECKIT END -->

diff --git a/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/MapView.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/MapView.kt
index da85fc9500..cebaf39316 100644
--- a/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/MapView.kt
+++ b/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/MapView.kt
@@ -34,11 +34,13 @@ import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.Checkbox
import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuGroup
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.MenuDefaults
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Slider
import androidx.compose.material3.Surface
@@ -723,69 +725,71 @@ private fun FdroidMainMapFilterDropdown(
mapFilterState: MapFilterState,
mapViewModel: MapViewModel,
) {
+ @OptIn(ExperimentalMaterial3ExpressiveApi::class)
DropdownMenu(
expanded = expanded,
onDismissRequest = onDismissRequest,
modifier = Modifier.background(MaterialTheme.colorScheme.surface),
) {
- DropdownMenuItem(
- text = {
- Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
- Icon(
- imageVector = MeshtasticIcons.Favorite,
- contentDescription = null,
- modifier = Modifier.padding(end = 8.dp),
- tint = MaterialTheme.colorScheme.onSurface,
- )
- Text(text = stringResource(Res.string.only_favorites), modifier = Modifier.weight(1f))
- Checkbox(
- checked = mapFilterState.onlyFavorites,
- onCheckedChange = { mapViewModel.toggleOnlyFavorites() },
- modifier = Modifier.padding(start = 8.dp),
- )
- }
- },
- onClick = { mapViewModel.toggleOnlyFavorites() },
- )
- DropdownMenuItem(
- text = {
- Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
- Icon(
- imageVector = MeshtasticIcons.PinDrop,
- contentDescription = null,
- modifier = Modifier.padding(end = 8.dp),
- tint = MaterialTheme.colorScheme.onSurface,
- )
- Text(text = stringResource(Res.string.show_waypoints), modifier = Modifier.weight(1f))
- Checkbox(
- checked = mapFilterState.showWaypoints,
- onCheckedChange = { mapViewModel.toggleShowWaypointsOnMap() },
- modifier = Modifier.padding(start = 8.dp),
- )
- }
- },
- onClick = { mapViewModel.toggleShowWaypointsOnMap() },
- )
- DropdownMenuItem(
- text = {
- Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
- Icon(
- imageVector = MeshtasticIcons.Lens,
- contentDescription = null,
- modifier = Modifier.padding(end = 8.dp),
- tint = MaterialTheme.colorScheme.onSurface,
- )
- Text(text = stringResource(Res.string.show_precision_circle), modifier = Modifier.weight(1f))
- Checkbox(
- checked = mapFilterState.showPrecisionCircle,
- onCheckedChange = { mapViewModel.toggleShowPrecisionCircleOnMap() },
- modifier = Modifier.padding(start = 8.dp),
- )
- }
- },
- onClick = { mapViewModel.toggleShowPrecisionCircleOnMap() },
- )
- HorizontalDivider()
+ DropdownMenuGroup(shapes = MenuDefaults.groupShapes()) {
+ DropdownMenuItem(
+ text = {
+ Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
+ Icon(
+ imageVector = MeshtasticIcons.Favorite,
+ contentDescription = null,
+ modifier = Modifier.padding(end = 8.dp),
+ tint = MaterialTheme.colorScheme.onSurface,
+ )
+ Text(text = stringResource(Res.string.only_favorites), modifier = Modifier.weight(1f))
+ Checkbox(
+ checked = mapFilterState.onlyFavorites,
+ onCheckedChange = { mapViewModel.toggleOnlyFavorites() },
+ modifier = Modifier.padding(start = 8.dp),
+ )
+ }
+ },
+ onClick = { mapViewModel.toggleOnlyFavorites() },
+ )
+ DropdownMenuItem(
+ text = {
+ Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
+ Icon(
+ imageVector = MeshtasticIcons.PinDrop,
+ contentDescription = null,
+ modifier = Modifier.padding(end = 8.dp),
+ tint = MaterialTheme.colorScheme.onSurface,
+ )
+ Text(text = stringResource(Res.string.show_waypoints), modifier = Modifier.weight(1f))
+ Checkbox(
+ checked = mapFilterState.showWaypoints,
+ onCheckedChange = { mapViewModel.toggleShowWaypointsOnMap() },
+ modifier = Modifier.padding(start = 8.dp),
+ )
+ }
+ },
+ onClick = { mapViewModel.toggleShowWaypointsOnMap() },
+ )
+ DropdownMenuItem(
+ text = {
+ Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
+ Icon(
+ imageVector = MeshtasticIcons.Lens,
+ contentDescription = null,
+ modifier = Modifier.padding(end = 8.dp),
+ tint = MaterialTheme.colorScheme.onSurface,
+ )
+ Text(text = stringResource(Res.string.show_precision_circle), modifier = Modifier.weight(1f))
+ Checkbox(
+ checked = mapFilterState.showPrecisionCircle,
+ onCheckedChange = { mapViewModel.toggleShowPrecisionCircleOnMap() },
+ modifier = Modifier.padding(start = 8.dp),
+ )
+ }
+ },
+ onClick = { mapViewModel.toggleShowPrecisionCircleOnMap() },
+ )
+ }
Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) {
val filterOptions = LastHeardFilter.entries
val selectedIndex = filterOptions.indexOf(mapFilterState.lastHeardFilter)

diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/map/component/MapFilterDropdown.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/component/MapFilterDropdown.kt
index 04112a411f..8c8d63c2ac 100644
--- a/androidApp/src/google/kotlin/org/meshtastic/app/map/component/MapFilterDropdown.kt
+++ b/androidApp/src/google/kotlin/org/meshtastic/app/map/component/MapFilterDropdown.kt
@@ -14,16 +14,20 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
+
package org.meshtastic.app.map.component
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Checkbox
import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuGroup
import androidx.compose.material3.DropdownMenuItem
-import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.MenuDefaults
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -52,55 +56,56 @@ import kotlin.math.roundToInt
internal fun MapFilterDropdown(expanded: Boolean, onDismissRequest: () -> Unit, mapViewModel: MapViewModel) {
val mapFilterState by mapViewModel.mapFilterStateFlow.collectAsStateWithLifecycle()
DropdownMenu(expanded = expanded, onDismissRequest = onDismissRequest) {
- DropdownMenuItem(
- text = { Text(stringResource(Res.string.only_favorites)) },
- onClick = { mapViewModel.toggleOnlyFavorites() },
- leadingIcon = {
- Icon(
- imageVector = MeshtasticIcons.Favorite,
- contentDescription = stringResource(Res.string.only_favorites),
- )
- },
- trailingIcon = {
- Checkbox(
- checked = mapFilterState.onlyFavorites,
- onCheckedChange = { mapViewModel.toggleOnlyFavorites() },
- )
- },
- )
- DropdownMenuItem(
- text = { Text(stringResource(Res.string.show_waypoints)) },
- onClick = { mapViewModel.toggleShowWaypointsOnMap() },
- leadingIcon = {
- Icon(
- imageVector = MeshtasticIcons.PinDrop,
- contentDescription = stringResource(Res.string.show_waypoints),
- )
- },
- trailingIcon = {
- Checkbox(
- checked = mapFilterState.showWaypoints,
- onCheckedChange = { mapViewModel.toggleShowWaypointsOnMap() },
- )
- },
- )
- DropdownMenuItem(
- text = { Text(stringResource(Res.string.show_precision_circle)) },
- onClick = { mapViewModel.toggleShowPrecisionCircleOnMap() },
- leadingIcon = {
- Icon(
- imageVector = MeshtasticIcons.Lens,
- contentDescription = stringResource(Res.string.show_precision_circle),
- )
- },
- trailingIcon = {
- Checkbox(
- checked = mapFilterState.showPrecisionCircle,
- onCheckedChange = { mapViewModel.toggleShowPrecisionCircleOnMap() },
- )
- },
- )
- HorizontalDivider()
+ DropdownMenuGroup(shapes = MenuDefaults.groupShapes()) {
+ DropdownMenuItem(
+ text = { Text(stringResource(Res.string.only_favorites)) },
+ onClick = { mapViewModel.toggleOnlyFavorites() },
+ leadingIcon = {
+ Icon(
+ imageVector = MeshtasticIcons.Favorite,
+ contentDescription = stringResource(Res.string.only_favorites),
+ )
+ },
+ trailingIcon = {
+ Checkbox(
+ checked = mapFilterState.onlyFavorites,
+ onCheckedChange = { mapViewModel.toggleOnlyFavorites() },
+ )
+ },
+ )
+ DropdownMenuItem(
+ text = { Text(stringResource(Res.string.show_waypoints)) },
+ onClick = { mapViewModel.toggleShowWaypointsOnMap() },
+ leadingIcon = {
+ Icon(
+ imageVector = MeshtasticIcons.PinDrop,
+ contentDescription = stringResource(Res.string.show_waypoints),
+ )
+ },
+ trailingIcon = {
+ Checkbox(
+ checked = mapFilterState.showWaypoints,
+ onCheckedChange = { mapViewModel.toggleShowWaypointsOnMap() },
+ )
+ },
+ )
+ DropdownMenuItem(
+ text = { Text(stringResource(Res.string.show_precision_circle)) },
+ onClick = { mapViewModel.toggleShowPrecisionCircleOnMap() },
+ leadingIcon = {
+ Icon(
+ imageVector = MeshtasticIcons.Lens,
+ contentDescription = stringResource(Res.string.show_precision_circle),
+ )
+ },
+ trailingIcon = {
+ Checkbox(
+ checked = mapFilterState.showPrecisionCircle,
+ onCheckedChange = { mapViewModel.toggleShowPrecisionCircleOnMap() },
+ )
+ },
+ )
+ }
Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) {
val filterOptions = LastHeardFilter.entries
val selectedIndex = filterOptions.indexOf(mapFilterState.lastHeardFilter)

diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/map/component/MapTypeDropdown.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/component/MapTypeDropdown.kt
index a2a1edb3cf..a649e29624 100644
--- a/androidApp/src/google/kotlin/org/meshtastic/app/map/component/MapTypeDropdown.kt
+++ b/androidApp/src/google/kotlin/org/meshtastic/app/map/component/MapTypeDropdown.kt
@@ -14,12 +14,16 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
+
package org.meshtastic.app.map.component
import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuGroup
import androidx.compose.material3.DropdownMenuItem
-import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
+import androidx.compose.material3.MenuDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -58,38 +62,16 @@ internal fun MapTypeDropdown(
)
DropdownMenu(expanded = expanded, onDismissRequest = onDismissRequest) {
- googleMapTypes.forEach { (name, type) ->
- DropdownMenuItem(
- text = { Text(name) },
- onClick = {
- mapViewModel.setSelectedGoogleMapType(type)
- onDismissRequest() // Close menu
- },
- trailingIcon =
- if (selectedCustomUrl == null && selectedGoogleMapType == type) {
- {
- Icon(
- MeshtasticIcons.Check,
- contentDescription = stringResource(Res.string.selected_map_type),
- )
- }
- } else {
- null
- },
- )
- }
-
- if (customTileProviders.isNotEmpty()) {
- HorizontalDivider()
- customTileProviders.forEach { config ->
+ DropdownMenuGroup(shapes = MenuDefaults.groupShapes()) {
+ googleMapTypes.forEach { (name, type) ->
DropdownMenuItem(
- text = { Text(config.name) },
+ text = { Text(name) },
onClick = {
- mapViewModel.selectCustomTileProvider(config)
- onDismissRequest() // Close menu
+ mapViewModel.setSelectedGoogleMapType(type)
+ onDismissRequest()
},
trailingIcon =
- if (selectedCustomUrl == config.urlTemplate) {
+ if (selectedCustomUrl == null && selectedGoogleMapType == type) {
{
Icon(
MeshtasticIcons.Check,
@@ -102,7 +84,31 @@ internal fun MapTypeDropdown(
)
}
}
- HorizontalDivider()
+
+ if (customTileProviders.isNotEmpty()) {
+ DropdownMenuGroup(shapes = MenuDefaults.groupShapes()) {
+ customTileProviders.forEach { config ->
+ DropdownMenuItem(
+ text = { Text(config.name) },
+ onClick = {
+ mapViewModel.selectCustomTileProvider(config)
+ onDismissRequest()
+ },
+ trailingIcon =
+ if (selectedCustomUrl == config.urlTemplate) {
+ {
+ Icon(
+ MeshtasticIcons.Check,
+ contentDescription = stringResource(Res.string.selected_map_type),
+ )
+ }
+ } else {
+ null
+ },
+ )
+ }
+ }
+ }
DropdownMenuItem(
text = { Text(stringResource(Res.string.manage_custom_tile_sources)) },
onClick = {

diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/emoji/CustomEmojiPrefsImpl.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/emoji/CustomEmojiPrefsImpl.kt
index b88994571b..5c05e31d7d 100644
--- a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/emoji/CustomEmojiPrefsImpl.kt
+++ b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/emoji/CustomEmojiPrefsImpl.kt
@@ -19,6 +19,7 @@ package org.meshtastic.core.prefs.emoji
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
@@ -42,6 +43,9 @@ class CustomEmojiPrefsImpl(
override val customEmojiFrequency: StateFlow<String?> =
dataStore.data.map { it[KEY_EMOJI_FREQ_PREF] }.stateIn(scope, SharingStarted.Eagerly, null)
+ override val preferredSkinToneIndex: StateFlow<Int> =
+ dataStore.data.map { it[KEY_SKIN_TONE_PREF] ?: 0 }.stateIn(scope, SharingStarted.Eagerly, 0)
+
override fun setCustomEmojiFrequency(frequency: String?) {
scope.launch {
dataStore.edit { prefs ->
@@ -54,8 +58,13 @@ class CustomEmojiPrefsImpl(
}
}
+ override fun setPreferredSkinToneIndex(index: Int) {
+ scope.launch { dataStore.edit { prefs -> prefs[KEY_SKIN_TONE_PREF] = index } }
+ }
+
companion object {
const val KEY_EMOJI_FREQ = "pref_key_custom_emoji_freq"
val KEY_EMOJI_FREQ_PREF = stringPreferencesKey(KEY_EMOJI_FREQ)
+ val KEY_SKIN_TONE_PREF = intPreferencesKey("pref_key_skin_tone")
}
}

diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/AppPreferences.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/AppPreferences.kt
index bcbaad63b2..92f9381e39 100644
--- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/AppPreferences.kt
+++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/AppPreferences.kt
@@ -65,8 +65,11 @@ interface MeshLogPrefs {
/** Reactive interface for emoji preferences. */
interface CustomEmojiPrefs {
val customEmojiFrequency: StateFlow<String?>
+ val preferredSkinToneIndex: StateFlow<Int>
fun setCustomEmojiFrequency(frequency: String?)
+
+ fun setPreferredSkinToneIndex(index: Int)
}
/** Reactive interface for general UI preferences. */

diff --git a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeAppPreferences.kt b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeAppPreferences.kt
index 5cb6f5ce1d..7122b1cc9d 100644
--- a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeAppPreferences.kt
+++ b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeAppPreferences.kt
@@ -64,10 +64,15 @@ class FakeFilterPrefs : FilterPrefs {
class FakeCustomEmojiPrefs : CustomEmojiPrefs {
override val customEmojiFrequency = MutableStateFlow<String?>(null)
+ override val preferredSkinToneIndex = MutableStateFlow(0)
override fun setCustomEmojiFrequency(frequency: String?) {
customEmojiFrequency.value = frequency
}
+
+ override fun setPreferredSkinToneIndex(index: Int) {
+ preferredSkinToneIndex.value = index
+ }
}
@Suppress("TooManyFunctions")

diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts
index 5453ea2f23..fc48e919a1 100644
--- a/core/ui/build.gradle.kts
+++ b/core/ui/build.gradle.kts
@@ -18,6 +18,7 @@
plugins {
alias(libs.plugins.meshtastic.kmp.library)
alias(libs.plugins.meshtastic.kmp.library.compose)
+ alias(libs.plugins.meshtastic.kotlinx.serialization)
id("meshtastic.kmp.jvm.android")
id("meshtastic.koin")
}
@@ -44,6 +45,7 @@ kotlin {
api(libs.compose.multiplatform.ui.tooling.preview)
implementation(libs.kermit)
+ implementation(libs.kotlinx.serialization.json)
implementation(libs.koin.compose.viewmodel)
implementation(libs.qrcode.kotlin)
implementation(libs.jetbrains.compose.material3.adaptive)

diff --git a/core/ui/src/androidMain/kotlin/org/meshtastic/core/ui/component/TimeTickWithLifecycle.kt b/core/ui/src/androidMain/kotlin/org/meshtastic/core/ui/component/TimeTickWithLifecycle.kt
deleted file mode 100644
index 7912c05b65..0000000000
--- a/core/ui/src/androidMain/kotlin/org/meshtastic/core/ui/component/TimeTickWithLifecycle.kt
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Copyright (c) 2026 Meshtastic LLC
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
-package org.meshtastic.core.ui.component
-
-import android.content.BroadcastReceiver
-import android.content.Context
-import android.content.Intent
-import android.content.IntentFilter
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableLongStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.platform.LocalContext
-import org.meshtastic.core.common.util.nowMillis
-
-@Composable
-actual fun rememberTimeTickWithLifecycle(): Long {
- val context = LocalContext.current
- var value by remember { mutableLongStateOf(nowMillis) }
-
- DisposableEffect(context) {
- val receiver =
- object : BroadcastReceiver() {
- override fun onReceive(context: Context, intent: Intent) {
- value = nowMillis
- }
- }
-
- androidx.core.content.ContextCompat.registerReceiver(
- context,
- receiver,
- IntentFilter(Intent.ACTION_TIME_TICK),
- androidx.core.content.ContextCompat.RECEIVER_NOT_EXPORTED,
- )
-
- onDispose { context.unregisterReceiver(receiver) }
- }
-
- return value
-}

diff --git a/core/ui/src/commonMain/composeResources/files/emoji-data.json b/core/ui/src/commonMain/composeResources/files/emoji-data.json
new file mode 100644
index 0000000000..918c06cc60
--- /dev/null
+++ b/core/ui/src/commonMain/composeResources/files/emoji-data.json
@@ -0,0 +1 @@
+{"version":"16.0","categories":[{"name":"Smileys & Emotion","icon":"😀","emojis":[{"b":"😀","k":["cheerful","cheery","face","grin","grinning","happy","laugh","nice","smile","smiling","teeth"]},{"b":"😃","k":["awesome","big","eyes","face","grin","grinning","haha","happy","joy","mouth","open","smile","smiley","smiling","teeth","with","yay"]},{"b":"😄","k":["eye","eyes","face","grin","grinning","happy","joy","laugh","lol","mouth","open","pleased","smile","smiling","with"]},{"b":"😁","k":["beaming","eye","eyes","face","grin","grinning","happy","nice","smile","smiling","teeth","with"]},{"b":"😆","k":["closed","eyes","face","grinning","haha","hahaha","happy","laugh","laughing","lol","mouth","open","rofl","satisfied","smile","smiling","squinting"]},{"b":"😅","k":["cold","dejected","excited","face","grinning","hot","mouth","nervous","open","smile","smiling","stress","stressed","sweat","with"]},{"b":"🤣","k":["crying","face","floor","funny","haha","happy","hehe","hilarious","joy","laugh","laughing","lmao","lol","rofl","roflmao","rolling","tear","the"]},{"b":"😂","k":["crying","face","feels","funny","haha","happy","hehe","hilarious","joy","laugh","lmao","lol","rofl","roflmao","tear","tears","with"]},{"b":"🙂","k":["face","happy","slightly","smile","smiling"]},{"b":"🙃","k":["down","face","hehe","smile","upside","upside-down"]},{"b":"🫠","k":["disappear","dissolve","dread","embarrassed","face","haha","heat","hot","liquid","lol","melt","melting","sarcasm","sarcastic"]},{"b":"😉","k":["face","flirt","heartbreaker","sexy","slide","tease","wink","winking","winks"]},{"b":"😊","k":["blush","eye","eyes","face","glad","proud","satisfied","smile","smiling","with"]},{"b":"😇","k":["angel","angelic","angels","blessed","face","fairy","fairytale","fantasy","halo","happy","innocent","peaceful","smile","smiling","spirit","tale","with"]},{"b":"🥰","k":["3","adore","crush","face","heart","hearts","ily","love","romance","smile","smiling","three","with","you"]},{"b":"😍","k":["143","bae","crush","eye","eyes","face","feels","heart","heart-eyes","hearts","ily","kisses","love","romance","romantic","smile","smiling","with","xoxo"]},{"b":"🤩","k":["excited","eyes","face","grinning","smile","star","star-struck","starry-eyed","struck","wow"]},{"b":"😘","k":["adorbs","bae","blowing","face","flirt","heart","ily","kiss","kissing","love","lover","miss","muah","romantic","smooch","xoxo","you"]},{"b":"😗","k":["143","date","dating","face","flirt","ily","kiss","kissing","love","smooch","smooches","xoxo","you"]},{"b":"☺️","k":["blush","face","pleased","relaxed","smiling"]},{"b":"😚","k":["143","bae","blush","closed","date","dating","eye","eyes","face","flirt","ily","kisses","kissing","smooches","with","xoxo"]},{"b":"😙","k":["143","closed","date","dating","eye","eyes","face","flirt","ily","kiss","kisses","kissing","love","night","smile","smiling","with"]},{"b":"🥲","k":["face","glad","grateful","happy","joy","pain","proud","relieved","smile","smiley","smiling","tear","touched","with"]},{"b":"😋","k":["delicious","eat","face","food","full","hungry","lick","savor","savoring","smile","smiling","tasty","tongue","um","yum","yummy"]},{"b":"😛","k":["awesome","cool","face","nice","out","party","stuck","stuck-out","sweet","tongue","with"]},{"b":"😜","k":["crazy","epic","eye","face","funny","joke","loopy","nutty","out","party","prank","silly","stuck","stuck-out","tongue","wacky","weirdo","wink","winking","with","yolo"]},{"b":"🤪","k":["crazy","eye","eyes","face","goofy","large","small","wacky","zany"]},{"b":"😝","k":["closed","eye","eyes","face","gross","horrible","omg","out","prank","squinting","stuck","stuck-out","taste","tongue","whatever","with","yolo"]},{"b":"🤑","k":["face","money","money-mouth","mouth","paid","rich"]},{"b":"🤗","k":["face","hands","hug","hugging","hugs","open","smiling","with"]},{"b":"🤭","k":["face","giggle","giggling","hand","mouth","oops","over","quiet","realization","secret","shock","sudden","surprise","whoops","with"]},{"b":"🫢","k":["amazement","and","awe","disbelief","embarrass","eyes","face","gasp","hand","mouth","omg","open","over","quiet","scared","shock","surprise","with"]},{"b":"🫣","k":["captivated","embarrass","eye","face","hide","hiding","peek","peeking","peep","scared","shy","stare","with"]},{"b":"🤫","k":["face","quiet","shh","shush","shushing","silence"]},{"b":"🤔","k":["chin","consider","face","hmm","ponder","pondering","thinking","wondering"]},{"b":"🫡","k":["face","good","luck","ma’am","ok","respect","salute","saluting","sir","troops","yes"]},{"b":"🤐","k":["face","hush","keep","mouth","quiet","secret","shut","silence","zip","zipper","zipper-mouth"]},{"b":"🤨","k":["disapproval","disbelief","distrust","emoji","eyebrow","face","hmm","mild","raised","skeptic","skeptical","skepticism","surprise","suspicious","what","with"]},{"b":"😐","k":["awkward","blank","deadpan","expressionless","face","fine","jealous","meh","neutral","oh","shade","straight","unamused","unhappy","unimpressed","whatever"]},{"b":"😑","k":["awkward","dead","expressionless","face","fine","inexpressive","jealous","meh","not","oh","omg","straight","uh","unhappy","unimpressed","whatever"]},{"b":"😶","k":["awkward","blank","expressionless","face","mouth","mouthless","mute","no","quiet","secret","silence","silent","speechless","without"]},{"b":"🫥","k":["depressed","disappear","dotted","face","hidden","hide","introvert","invisible","line","meh","whatever","wtv"]},{"b":"😶‍🌫️","k":["clouds","face","in"]},{"b":"😏","k":["boss","dapper","face","flirt","homie","kidding","leer","shade","slick","sly","smirk","smirking","smug","snicker","suave","suspicious","swag"]},{"b":"😒","k":["...","bored","face","fine","jealous","jel","jelly","meh","pissed","smh","ugh","uhh","unamused","unhappy","weird","whatever"]},{"b":"🙄","k":["eyeroll","eyes","face","roll","rolling","shade","ugh","whatever","with"]},{"b":"😬","k":["awk","awkward","dentist","face","grimace","grimacing","grinning","smile","smiling"]},{"b":"😮‍💨","k":["blow","blowing","exhale","exhaling","exhausted","face","gasp","groan","relief","sigh","smiley","smoke","whisper","whistle"]},{"b":"🤥","k":["face","liar","lie","lying","pinocchio"]},{"b":"🫨","k":["crazy","daze","earthquake","face","omg","panic","shaking","shock","surprise","vibrate","whoa","wow"]},{"b":"😌","k":["calm","face","peace","relief","relieved","whew","zen"]},{"b":"😔","k":["awful","bored","dejected","died","disappointed","face","losing","lost","pensive","sad","sucks"]},{"b":"😪","k":["crying","face","good","night","sad","sleep","sleeping","sleepy","tired"]},{"b":"🤤","k":["drooling","face"]},{"b":"😴","k":["bed","bedtime","face","good","goodnight","nap","night","sleep","sleeping","tired","whatever","yawn","zzz"]},{"b":"😷","k":["cold","dentist","dermatologist","doctor","dr","face","germs","ill","mask","medical","medicine","sick","with"]},{"b":"🤒","k":["face","ill","sick","thermometer","with"]},{"b":"🤕","k":["bandage","face","head","head-bandage","hurt","injury","ouch","with"]},{"b":"🤢","k":["barf","disgusted","face","gross","nasty","nauseated","sick","vomit"]},{"b":"🤮","k":["barf","ew","face","gross","puke","sick","spew","throw","up","vomit","vomiting"]},{"b":"🤧","k":["achoo","face","fever","flu","gesundheit","sick","sneeze","sneezing"]},{"b":"🥵","k":["dying","face","feverish","heat","hot","panting","red-faced","stroke","sweating","tongue"]},{"b":"🥶","k":["blue","blue-faced","cold","face","freezing","frostbite","ice","icicles","subzero","teeth"]},{"b":"🥴","k":["dizzy","drunk","eyes","face","groggy","intoxicated","mouth","tipsy","uneven","wavy","woozy"]},{"b":"😵","k":["crossed-out","dead","dizzy","eyes","face","feels","knocked","out","sick","tired","with"]},{"b":"😵‍💫","k":["confused","dizzy","eyes","face","hypnotized","omg","smiley","spiral","trouble","whoa","with","woah","woozy"]},{"b":"🤯","k":["blown","explode","exploding","head","mind","mindblown","no","shocked","way"]},{"b":"🤠","k":["cowboy","cowgirl","face","hat"]},{"b":"🥳","k":["bday","birthday","celebrate","celebration","excited","face","happy","hat","hooray","horn","party","partying"]},{"b":"🥸","k":["disguise","disguised","eyebrow","face","glasses","incognito","moustache","mustache","nose","person","spy","tache","tash"]},{"b":"😎","k":["awesome","beach","bright","bro","chilling","cool","face","rad","relaxed","shades","slay","smile","smiling","style","sunglasses","swag","win","with"]},{"b":"🤓","k":["brainy","clever","expert","face","geek","gifted","glasses","intelligent","nerd","smart"]},{"b":"🧐","k":["classy","face","fancy","monocle","rich","stuffy","wealthy","with"]},{"b":"😕","k":["befuddled","confused","confusing","dunno","face","frown","hm","meh","not","sad","sorry","sure"]},{"b":"🫤","k":["confused","confusion","diagonal","disappointed","doubt","doubtful","face","frustrated","frustration","meh","mouth","skeptical","unsure","whatever","with","wtv"]},{"b":"😟","k":["anxious","butterflies","face","nerves","nervous","sad","stress","stressed","surprised","worried","worry"]},{"b":"🙁","k":["face","frown","frowning","sad","slightly"]},{"b":"☹️","k":["face","frowning"]},{"b":"😮","k":["believe","face","forgot","impressed","mouth","omg","open","shocked","surprise","surprised","sympathy","unbelievable","unreal","whoa","with","wow","you"]},{"b":"😯","k":["epic","face","hushed","omg","silence","speechless","stunned","surprised","whoa","woah"]},{"b":"😲","k":["amazed","astonished","cost","face","gasp","no","omg","shocked","totally","way"]},{"b":"😳","k":["amazed","awkward","crazy","dazed","dead","disbelief","embarrassed","face","flushed","geez","heat","hot","impressed","jeez","what","wow"]},{"b":"🥺","k":["begging","big","eyes","face","mercy","not","pleading","please","pretty","puppy","sad","why"]},{"b":"🥹","k":["admiration","aww","back","cry","embarrassed","face","feelings","grateful","gratitude","holding","joy","please","proud","resist","sad","tears"]},{"b":"😦","k":["caught","face","frown","frowning","guard","mouth","open","scared","scary","surprise","what","with","wow"]},{"b":"😧","k":["anguished","face","forgot","scared","scary","stressed","stunned","surprise","unhappy","what","wow"]},{"b":"😨","k":["afraid","anxious","blame","face","fear","fearful","oops","scared","shocked","worried"]},{"b":"😰","k":["anxious","blue","cold","eek","face","mouth","nervous","open","rushed","scared","sweat","with","yikes"]},{"b":"😥","k":["anxious","but","call","close","complicated","disappointed","face","nervous","not","phew","relieved","sad","sweat","time","whew"]},{"b":"😢","k":["awful","cry","crying","face","feels","miss","sad","tear","triste","unhappy"]},{"b":"😭","k":["bawling","cry","crying","face","loudly","sad","sob","tear","tears","unhappy"]},{"b":"😱","k":["epic","face","fear","fearful","horror","munch","scared","scream","screamer","screaming","shocked","surprised","woah"]},{"b":"😖","k":["annoyed","confounded","confused","cringe","distraught","face","feels","frustrated","mad","sad"]},{"b":"😣","k":["concentrate","concentration","face","focus","headache","persevere","persevering","struggling"]},{"b":"😞","k":["awful","blame","dejected","disappointed","face","fail","losing","sad","unhappy"]},{"b":"😓","k":["close","cold","downcast","face","feels","headache","nervous","sad","scared","sweat","with","yikes"]},{"b":"😩","k":["crying","face","fail","feels","hungry","mad","nooo","sad","sleepy","tired","unhappy","weary"]},{"b":"😫","k":["cost","face","feels","nap","sad","sneeze","tired","upset","whine"]},{"b":"🥱","k":["bedtime","bored","face","goodnight","nap","night","sleep","sleepy","tired","whatever","yawn","yawning","zzz"]},{"b":"😤","k":["anger","angry","face","feels","from","fume","fuming","furious","fury","mad","nose","smug","steam","triumph","unhappy","with","won"]},{"b":"😡","k":["anger","angry","enraged","face","feels","mad","maddening","pout","pouting","rage","red","shade","unhappy","upset"]},{"b":"😠","k":["anger","angry","annoyed","blame","face","feels","frustrated","mad","maddening","rage","shade","unhappy","upset"]},{"b":"🤬","k":["censor","cursing","cussing","face","foul","mad","mouth","pissed","swearing","symbols","with"]},{"b":"😈","k":["demon","devil","evil","face","fairy","fairytale","fantasy","horns","imp","purple","shade","smile","smiling","tale","with"]},{"b":"👿","k":["angry","demon","devil","evil","face","fairy","fairytale","fantasy","horns","imp","mischievous","purple","shade","tale","with"]},{"b":"💀","k":["body","danger","dead","death","face","fairy","fairytale","i’m","lmao","monster","poison","skull","tale","yolo"]},{"b":"☠️","k":["and","crossbones","danger","pirate","skull"]},{"b":"💩","k":["bs","comic","crap","doo","dung","face","fml","hankey","monster","pile","poo","poop","shit","smelly","smh","stink","stinks","stinky","turd"]},{"b":"🤡","k":["clown","face"]},{"b":"👹","k":["creature","devil","face","fairy","fairytale","fantasy","japanese","mask","monster","ogre","scary","tale"]},{"b":"👺","k":["angry","creature","face","fairy","fairytale","fantasy","goblin","japanese","mask","mean","monster","tale"]},{"b":"👻","k":["boo","creature","excited","face","fairy","fairytale","fantasy","ghost","halloween","haunting","monster","scary","silly","tale"]},{"b":"👽","k":["alien","creature","extraterrestrial","face","fairy","fairytale","fantasy","monster","space","tale","ufo"]},{"b":"👾","k":["alien","creature","extraterrestrial","face","fairy","fairytale","fantasy","game","gamer","games","invader","monster","pixelated","retro","space","tale","ufo"]},{"b":"🤖","k":["face","monster","robot"]},{"b":"😺","k":["animal","cat","face","grinning","mouth","open","smile","smiley","smiling"]},{"b":"😸","k":["animal","cat","eye","eyes","face","grin","grinning","smile","smiling","with"]},{"b":"😹","k":["animal","cat","face","joy","laugh","laughing","lol","tear","tears","with"]},{"b":"😻","k":["animal","cat","eye","eyes","face","heart","heart-eyes","love","smile","smiling","with"]},{"b":"😼","k":["animal","cat","face","ironic","smile","smirk","with","wry"]},{"b":"😽","k":["animal","cat","closed","eye","eyes","face","kiss","kissing"]},{"b":"🙀","k":["animal","cat","face","horror","oh","scream","surprised","weary"]},{"b":"😿","k":["animal","cat","cry","crying","face","sad","tear"]},{"b":"😾","k":["animal","cat","face","pouting"]},{"b":"🙈","k":["blind","embarrassed","evil","face","forbidden","forgot","gesture","hide","ignore","monkey","no","omg","prohibited","scared","secret","see","see-no-evil","smh","watch"]},{"b":"🙉","k":["animal","deaf","ears","evil","face","forbidden","gesture","hear","hear-no-evil","listen","monkey","no","not","prohibited","secret","shh","tmi"]},{"b":"🙊","k":["animal","evil","face","forbidden","gesture","hush","monkey","mute","no","not","oops","prohibited","quiet","secret","speak","speak-no-evil","stealth"]},{"b":"💌","k":["email","envelope","heart","letter","love","mail","romance","valentine"]},{"b":"💘","k":["143","adorbs","arrow","cupid","date","emotion","heart","ily","love","romance","valentine","with"]},{"b":"💝","k":["143","anniversary","chocolates","emotion","gift","heart","ily","kisses","ribbon","valentine","with","xoxo"]},{"b":"💖","k":["143","emotion","excited","good","heart","ily","kisses","morning","night","sparkle","sparkling","xoxo"]},{"b":"💗","k":["143","emotion","excited","growing","heart","heartpulse","ily","kisses","muah","nervous","pulse","xoxo"]},{"b":"💓","k":["143","beating","cardio","emotion","heart","heartbeat","ily","love","pulsating","pulse"]},{"b":"💞","k":["143","adorbs","anniversary","emotion","heart","hearts","revolving"]},{"b":"💕","k":["143","anniversary","date","dating","emotion","heart","hearts","ily","kisses","love","loving","two","xoxo"]},{"b":"💟","k":["143","decoration","emotion","heart","hearth","purple","white"]},{"b":"❣️","k":["exclamation","heart","heavy"]},{"b":"💔","k":["break","broken","crushed","emotion","heart","heartbroken","lonely","sad"]},{"b":"❤️‍🔥","k":["fire","heart","on"]},{"b":"❤️‍🩹","k":["heart","mending"]},{"b":"❤️","k":["heart","love","red"]},{"b":"🩷","k":["143","adorable","cute","emotion","heart","ily","like","love","pink","special","sweet"]},{"b":"🧡","k":["143","heart","orange"]},{"b":"💛","k":["143","cardiac","emotion","heart","ily","love","yellow"]},{"b":"💚","k":["143","emotion","green","heart","ily","love","romantic"]},{"b":"💙","k":["143","blue","emotion","heart","ily","love","romance"]},{"b":"🩵","k":["143","blue","cute","cyan","emotion","heart","ily","light","like","love","sky","special","teal"]},{"b":"💜","k":["143","bestest","emotion","heart","ily","love","purple"]},{"b":"🤎","k":["143","brown","heart"]},{"b":"🖤","k":["black","evil","heart","wicked"]},{"b":"🩶","k":["143","emotion","gray","grey","heart","ily","love","silver","slate","special"]},{"b":"🤍","k":["143","heart","white"]},{"b":"💋","k":["dating","emotion","heart","kiss","kissing","lips","lipstick","mark","romance","sexy"]},{"b":"💯","k":["100","a+","agree","clearly","definitely","faithful","fleek","full","hundred","keep","perfect","point","points","score","true","truth","yup"]},{"b":"💢","k":["anger","angry","comic","mad","symbol","upset"]},{"b":"💥","k":["bomb","boom","collide","collision","comic","explode"]},{"b":"💫","k":["comic","dizzy","shining","shooting","star","stars"]},{"b":"💦","k":["comic","drip","droplet","droplets","drops","splashing","squirt","sweat","water","wet","work","workout"]},{"b":"💨","k":["away","blow","cloud","comic","dash","dashing","fart","fast","go","gone","gotta","running","smoke","wind"]},{"b":"🕳️","k":["hole"]},{"b":"💬","k":["balloon","bubble","comic","comment","dialog","message","sms","speech","talk","text","typing"]},{"b":"👁️‍🗨️","k":["bubble","eye","speech"]},{"b":"🗨️","k":["bubble","left","speech"]},{"b":"🗯️","k":["anger","bubble","right"]},{"b":"💭","k":["balloon","bubble","cartoon","cloud","comic","daydream","decisions","dream","idea","invent","invention","realize","think","thinking","thought","thoughts","wonder"]},{"b":"💤","k":["comic","good","goodnight","night","sleep","sleeping","sleepy","tired","zzz"]}]},{"name":"People & Body","icon":"👋","emojis":[{"b":"👋","k":["bye","cya","g2g","goodbye","greetings","gtg","hand","hello","hey","hi","later","outtie","ttfn","ttyl","wave","waving","yo","you"],"s":true},{"b":"🤚","k":["back","backhand","hand","of","raised"],"s":true},{"b":"🖐️","k":["fingers","hand","raised","splayed","with"],"s":true},{"b":"✋","k":["5","five","hand","high","highfive","raised","stop"],"s":true},{"b":"🖖","k":["finger","hand","hands","prosper","salute","spock","vulcan"],"s":true},{"b":"🫱","k":["hand","handshake","hold","reach","right","rightward","rightwards","shake"],"s":true},{"b":"🫲","k":["hand","handshake","hold","left","leftward","leftwards","reach","shake"],"s":true},{"b":"🫳","k":["dismiss","down","drop","dropped","hand","palm","pick","shoo","up"],"s":true},{"b":"🫴","k":["beckon","catch","come","hand","hold","know","lift","me","offer","palm","tell","up"],"s":true},{"b":"🫷","k":["block","five","halt","hand","high","hold","leftward","leftwards","pause","push","pushing","refuse","slap","stop","wait"],"s":true},{"b":"🫸","k":["block","five","halt","hand","high","hold","pause","push","pushing","refuse","rightward","rightwards","slap","stop","wait"],"s":true},{"b":"👌","k":["awesome","bet","dope","fleek","fosho","got","gotcha","hand","legit","ok","okay","pinch","rad","sure","sweet","three"],"s":true},{"b":"🤌","k":["fingers","gesture","hand","hold","huh","interrogation","patience","pinched","relax","sarcastic","ugh","what","zip"],"s":true},{"b":"🤏","k":["amount","bit","fingers","hand","little","pinching","small","sort"],"s":true},{"b":"✌️","k":["hand","peace","v","victory"],"s":true},{"b":"🤞","k":["cross","crossed","finger","fingers","hand","hopeful","luck"],"s":true},{"b":"🫰","k":["<3","and","crossed","expensive","finger","hand","heart","index","love","money","snap","thumb","with"],"s":true},{"b":"🤟","k":["fingers","gesture","hand","ily","love","love-you","three","you"],"s":true},{"b":"🤘","k":["finger","hand","horns","metal","rock-on","sign","the"],"s":true},{"b":"🤙","k":["call","hand","hang","loose","me","shaka"],"s":true},{"b":"👈","k":["backhand","finger","hand","index","left","point","pointing"],"s":true},{"b":"👉","k":["backhand","finger","hand","index","point","pointing","right"],"s":true},{"b":"👆","k":["2","backhand","finger","hand","index","point","pointing","up"],"s":true},{"b":"🖕","k":["finger","fu","hand","middle"],"s":true},{"b":"👇","k":["backhand","down","finger","hand","index","point","pointing"],"s":true},{"b":"☝️","k":["index","point","pointing","up"],"s":true},{"b":"🫵","k":["at","finger","hand","index","pointing","poke","the","viewer","you"],"s":true},{"b":"👍","k":["+1","approve","good","hand","like","ok","thumb","thumbs","thumbsup","up","yes"],"s":true},{"b":"👎","k":["-1","bad","bury","disapprove","dislike","down","good","hand","no","nope","thumb","thumbs","thumbsdown"],"s":true},{"b":"✊","k":["clenched","fist","hand","power","punch","raised","solidarity"],"s":true},{"b":"👊","k":["absolutely","agree","attack","boom","bro","bruh","bump","clenched","correct","facepunch","fist","hand","knuckle","oncoming","pound","punch","rock","ttyl"],"s":true},{"b":"🤛","k":["fist","left","left-facing","leftwards"],"s":true},{"b":"🤜","k":["fist","right","right-facing","rightwards"],"s":true},{"b":"👏","k":["applause","approval","awesome","clap","clapping","congrats","congratulations","excited","good","great","hand","hands","homie","job","nice","praise","prayed","well","yay"],"s":true},{"b":"🙌","k":["celebration","gesture","hand","hands","hooray","praise","raised","raising"],"s":true},{"b":"🫶","k":["<3","hands","heart","love","you"],"s":true},{"b":"👐","k":["hand","hands","hug","jazz","open","swerve"],"s":true},{"b":"🤲","k":["cupped","dua","hands","palms","pray","prayer","together","up","wish"],"s":true},{"b":"🤝","k":["agreement","deal","hand","handshake","meeting","shake"],"s":true},{"b":"🙏","k":["appreciate","ask","beg","blessed","bow","cmon","five","folded","gesture","hand","hands","high","hope","please","pray","thanks","thx","wish"],"s":true},{"b":"✍️","k":["hand","writing"],"s":true},{"b":"💅","k":["beauty","bored","care","cosmetics","done","makeup","manicure","nail","polish","whatever"],"s":true},{"b":"🤳","k":["camera","phone","selfie"],"s":true},{"b":"💪","k":["arm","beast","bench","bicep","biceps","bodybuilder","bro","curls","flex","flexed","gains","gym","jacked","muscle","press","ripped","strong","weightlift","workout"],"s":true},{"b":"🦾","k":["accessibility","arm","mechanical","prosthetic"]},{"b":"🦿","k":["accessibility","leg","mechanical","prosthetic"]},{"b":"🦵","k":["bent","foot","kick","knee","leg","limb"],"s":true},{"b":"🦶","k":["ankle","feet","foot","kick","stomp"],"s":true},{"b":"👂","k":["body","ear","ears","hear","hearing","listen","listening","sound"],"s":true},{"b":"🦻","k":["accessibility","aid","ear","hard","hearing","with"],"s":true},{"b":"👃","k":["body","nose","noses","nosey","odor","smell","smells"],"s":true},{"b":"🧠","k":["brain","intelligent","smart"]},{"b":"🫀","k":["anatomical","beat","cardiology","heart","heartbeat","organ","pulse","real","red"]},{"b":"🫁","k":["breath","breathe","exhalation","inhalation","lung","lungs","organ","respiration"]},{"b":"🦷","k":["dentist","pearly","teeth","tooth","white"]},{"b":"🦴","k":["bone","bones","dog","skeleton","wishbone"]},{"b":"👀","k":["body","eye","eyes","face","googly","look","looking","omg","peep","see","seeing","watch"]},{"b":"👁️","k":["eye"]},{"b":"👅","k":["body","lick","slurp","taste","tongue"]},{"b":"👄","k":["beauty","body","kiss","kissing","lips","lipstick","mouth"]},{"b":"🫦","k":["anxious","bite","biting","fear","flirt","flirting","kiss","lip","lipstick","nervous","sexy","uncomfortable","worried","worry"]},{"b":"👶","k":["babies","baby","child","children","goo","infant","newborn","pregnant","young"],"s":true},{"b":"🧒","k":["bright-eyed","child","grandchild","kid","young","younger"],"s":true},{"b":"👦","k":["boy","bright-eyed","child","grandson","kid","son","young","younger"],"s":true},{"b":"👧","k":["bright-eyed","child","daughter","girl","granddaughter","kid","virgo","young","younger","zodiac"],"s":true},{"b":"🧑","k":["adult","person"],"s":true},{"b":"👱","k":["blond","blond-haired","hair","haired","human","person","person:"],"s":true},{"b":"👨","k":["adult","bro","dad","father","man","mustache"],"s":true},{"b":"🧔","k":["beard","bearded","person","person:","whiskers"],"s":true},{"b":"🧔‍♂️","k":["beard","man","man:"],"s":true},{"b":"🧔‍♀️","k":["beard","woman","woman:"],"s":true},{"b":"👨‍🦰","k":["hair","haired","man","man:","red"],"s":true},{"b":"👨‍🦱","k":["curly","hair","haired","man","man:"],"s":true},{"b":"👨‍🦳","k":["hair","haired","man","man:","white"],"s":true},{"b":"👨‍🦲","k":["bald","man","man:"],"s":true},{"b":"👩","k":["adult","girls","lady","woman"],"s":true},{"b":"👩‍🦰","k":["hair","haired","red","woman","woman:"],"s":true},{"b":"🧑‍🦰","k":["hair","person","person:","red"],"s":true},{"b":"👩‍🦱","k":["curly","hair","haired","woman","woman:"],"s":true},{"b":"🧑‍🦱","k":["curly","hair","person","person:"],"s":true},{"b":"👩‍🦳","k":["hair","haired","white","woman","woman:"],"s":true},{"b":"🧑‍🦳","k":["hair","person","person:","white"],"s":true},{"b":"👩‍🦲","k":["bald","woman","woman:"],"s":true},{"b":"🧑‍🦲","k":["bald","person","person:"],"s":true},{"b":"👱‍♀️","k":["blond","blonde","hair","haired","woman","woman:"],"s":true},{"b":"👱‍♂️","k":["blond","hair","haired","man","man:"],"s":true},{"b":"🧓","k":["adult","elderly","grandparent","old","older","person","wise"],"s":true},{"b":"👴","k":["adult","bald","elderly","gramps","grandfather","grandpa","man","old","older","wise"],"s":true},{"b":"👵","k":["adult","elderly","grandma","grandmother","granny","lady","old","older","wise","woman"],"s":true},{"b":"🙍","k":["annoyed","disappointed","disgruntled","disturbed","frown","frowning","frustrated","gesture","irritated","person","upset"],"s":true},{"b":"🙍‍♂️","k":["frowning","man"],"s":true},{"b":"🙍‍♀️","k":["frowning","woman"],"s":true},{"b":"🙎","k":["disappointed","downtrodden","face","frown","grimace","person","pouting","scowl","sulk","upset","whine"],"s":true},{"b":"🙎‍♂️","k":["man","pouting"],"s":true},{"b":"🙎‍♀️","k":["pouting","woman"],"s":true},{"b":"🙅","k":["denied","forbidden","gesture","gesturing","good","halt","hand","no","not","person","prohibit","stop"],"s":true},{"b":"🙅‍♂️","k":["denied","gesturing","good","halt","man","ng","no","stop"],"s":true},{"b":"🙅‍♀️","k":["denied","gesturing","good","halt","ng","no","stop","woman"],"s":true},{"b":"🙆","k":["exercise","gesture","gesturing","hand","ok","omg","person"],"s":true},{"b":"🙆‍♂️","k":["gesturing","man","ok"],"s":true},{"b":"🙆‍♀️","k":["gesturing","ok","woman"],"s":true},{"b":"💁","k":["desk","fetch","flick","flip","gossip","hand","information","person","sarcasm","sarcastic","sassy","seriously","tipping","whatever"],"s":true},{"b":"💁‍♂️","k":["hand","information","man","sassy","tipping"],"s":true},{"b":"💁‍♀️","k":["hand","information","sassy","tipping","woman"],"s":true},{"b":"🙋","k":["gesture","hand","here","know","me","person","pick","question","raise","raising"],"s":true},{"b":"🙋‍♂️","k":["hand","man","raising"],"s":true},{"b":"🙋‍♀️","k":["hand","raising","woman"],"s":true},{"b":"🧏","k":["accessibility","deaf","ear","gesture","hear","person"],"s":true},{"b":"🧏‍♂️","k":["deaf","man"],"s":true},{"b":"🧏‍♀️","k":["deaf","woman"],"s":true},{"b":"🙇","k":["apology","ask","beg","bow","bowing","favor","forgive","gesture","meditate","meditation","person","pity","regret","respect","sorry","thanks"],"s":true},{"b":"🙇‍♂️","k":["bowing","man","respect","thanks"],"s":true},{"b":"🙇‍♀️","k":["bowing","respect","thanks","woman"],"s":true},{"b":"🤦","k":["again","bewilder","disbelief","exasperation","facepalm","facepalming","no","not","oh","omg","person","shock","smh"],"s":true},{"b":"🤦‍♂️","k":["facepalming","man"],"s":true},{"b":"🤦‍♀️","k":["facepalming","woman"],"s":true},{"b":"🤷","k":["doubt","dunno","guess","idk","ignorance","indifference","knows","maybe","person","shrug","shrugging","whatever","who"],"s":true},{"b":"🤷‍♂️","k":["man","shrugging"],"s":true},{"b":"🤷‍♀️","k":["shrugging","woman"],"s":true},{"b":"🧑‍⚕️","k":["health","worker"],"s":true},{"b":"👨‍⚕️","k":["doctor","health","man","nurse","worker"],"s":true},{"b":"👩‍⚕️","k":["doctor","health","nurse","woman","worker"],"s":true},{"b":"🧑‍🎓","k":["graduate","student"],"s":true},{"b":"👨‍🎓","k":["graduate","graduation","man","student"],"s":true},{"b":"👩‍🎓","k":["graduate","graduation","student","woman"],"s":true},{"b":"🧑‍🏫","k":["instructor","lecturer","professor","teacher"],"s":true},{"b":"👨‍🏫","k":["instructor","lecturer","man","professor","school","teacher"],"s":true},{"b":"👩‍🏫","k":["instructor","lecturer","professor","school","teacher","woman"],"s":true},{"b":"🧑‍⚖️","k":["judge"],"s":true},{"b":"👨‍⚖️","k":["judge","justice","man"],"s":true},{"b":"👩‍⚖️","k":["judge","justice","woman"],"s":true},{"b":"🧑‍🌾","k":["farmer","gardener","rancher"],"s":true},{"b":"👨‍🌾","k":["farmer","gardener","man","rancher"],"s":true},{"b":"👩‍🌾","k":["farmer","gardener","rancher","woman"],"s":true},{"b":"🧑‍🍳","k":["chef","cook"],"s":true},{"b":"👨‍🍳","k":["chef","cook","man"],"s":true},{"b":"👩‍🍳","k":["chef","cook","woman"],"s":true},{"b":"🧑‍🔧","k":["electrician","mechanic","plumber","tradesperson"],"s":true},{"b":"👨‍🔧","k":["electrician","man","mechanic","plumber","tradesperson"],"s":true},{"b":"👩‍🔧","k":["electrician","mechanic","plumber","tradesperson","woman"],"s":true},{"b":"🧑‍🏭","k":["assembly","factory","industrial","worker"],"s":true},{"b":"👨‍🏭","k":["assembly","factory","industrial","man","worker"],"s":true},{"b":"👩‍🏭","k":["assembly","factory","industrial","woman","worker"],"s":true},{"b":"🧑‍💼","k":["architect","business","manager","office","white-collar","worker"],"s":true},{"b":"👨‍💼","k":["architect","business","man","manager","office","white-collar","worker"],"s":true},{"b":"👩‍💼","k":["architect","business","manager","office","white-collar","woman","worker"],"s":true},{"b":"🧑‍🔬","k":["biologist","chemist","engineer","mathematician","physicist","scientist"],"s":true},{"b":"👨‍🔬","k":["biologist","chemist","engineer","man","mathematician","physicist","research","scientist"],"s":true},{"b":"👩‍🔬","k":["biologist","chemist","engineer","mathematician","physicist","research","scientist","woman"],"s":true},{"b":"🧑‍💻","k":["coder","computer","developer","inventor","software","technologist"],"s":true},{"b":"👨‍💻","k":["coder","computer","developer","inventor","man","software","technologist"],"s":true},{"b":"👩‍💻","k":["coder","computer","developer","inventor","software","technologist","woman"],"s":true},{"b":"🧑‍🎤","k":["actor","entertainer","rock","rockstar","singer","star"],"s":true},{"b":"👨‍🎤","k":["actor","entertainer","man","rock","rockstar","singer","star"],"s":true},{"b":"👩‍🎤","k":["actor","entertainer","rock","rockstar","singer","star","woman"],"s":true},{"b":"🧑‍🎨","k":["artist","palette"],"s":true},{"b":"👨‍🎨","k":["artist","man","painter","palette"],"s":true},{"b":"👩‍🎨","k":["artist","painter","palette","woman"],"s":true},{"b":"🧑‍✈️","k":["pilot"],"s":true},{"b":"👨‍✈️","k":["man","pilot"],"s":true},{"b":"👩‍✈️","k":["pilot","woman"],"s":true},{"b":"🧑‍🚀","k":["astronaut","rocket","space"],"s":true},{"b":"👨‍🚀","k":["astronaut","man","rocket","space"],"s":true},{"b":"👩‍🚀","k":["astronaut","rocket","space","woman"],"s":true},{"b":"🧑‍🚒","k":["fire","firefighter","firetruck"],"s":true},{"b":"👨‍🚒","k":["fire","firefighter","firetruck","man"],"s":true},{"b":"👩‍🚒","k":["fire","firefighter","firetruck","woman"],"s":true},{"b":"👮","k":["apprehend","arrest","citation","cop","law","officer","over","police","pulled","undercover"],"s":true},{"b":"👮‍♂️","k":["cop","law","man","officer","police","policeman"],"s":true},{"b":"👮‍♀️","k":["cop","law","officer","police","policewoman","woman"],"s":true},{"b":"🕵️","k":["detective","sleuth"],"s":true},{"b":"🕵️‍♂️","k":["detective","male","man","sleuth"],"s":true},{"b":"🕵️‍♀️","k":["detective","female","sleuth","woman"],"s":true},{"b":"💂","k":["buckingham","guard","helmet","london","palace"],"s":true},{"b":"💂‍♂️","k":["guard","guardsman","man"],"s":true},{"b":"💂‍♀️","k":["guard","guardswoman","woman"],"s":true},{"b":"🥷","k":["assassin","fight","fighter","hidden","ninja","person","secret","skills","sly","soldier","stealth","war"],"s":true},{"b":"👷","k":["build","construction","fix","hardhat","hat","helmet","man","person","rebuild","remodel","repair","work","worker"],"s":true},{"b":"👷‍♂️","k":["construction","helmet","man","worker"],"s":true},{"b":"👷‍♀️","k":["construction","helmet","woman","worker"],"s":true},{"b":"🫅","k":["crown","monarch","noble","person","regal","royal","royalty","with"],"s":true},{"b":"🤴","k":["crown","fairy","fairytale","fantasy","king","prince","royal","royalty","tale"],"s":true},{"b":"👸","k":["crown","fairy","fairytale","fantasy","princess","queen","royal","royalty","tale"],"s":true},{"b":"👳","k":["person","turban","wearing","with"],"s":true},{"b":"👳‍♂️","k":["man","turban","wearing","with"],"s":true},{"b":"👳‍♀️","k":["turban","wearing","with","woman"],"s":true},{"b":"👲","k":["cap","chinese","gua","guapi","hat","man","mao","person","pi","skullcap","with"],"s":true},{"b":"🧕","k":["bandana","head","headscarf","hijab","kerchief","mantilla","tichel","with","woman"],"s":true},{"b":"🤵","k":["formal","groom","in","marriage","person","tuxedo","wedding"],"s":true},{"b":"🤵‍♂️","k":["in","man","tuxedo"],"s":true},{"b":"🤵‍♀️","k":["in","tuxedo","woman"],"s":true},{"b":"👰","k":["marriage","person","veil","wedding","with"],"s":true},{"b":"👰‍♂️","k":["man","veil","with"],"s":true},{"b":"👰‍♀️","k":["bride","veil","with","woman"],"s":true},{"b":"🤰","k":["pregnant","woman"],"s":true},{"b":"🫃","k":["belly","bloated","full","man","overeat","pregnant"],"s":true},{"b":"🫄","k":["belly","bloated","full","overeat","person","pregnant","stuffed"],"s":true},{"b":"🤱","k":["baby","breast","breast-feeding","feeding","mom","mother","nursing","woman"],"s":true},{"b":"👩‍🍼","k":["baby","feed","feeding","mom","mother","nanny","newborn","nursing","woman"],"s":true},{"b":"👨‍🍼","k":["baby","dad","father","feed","feeding","man","nanny","newborn","nursing"],"s":true},{"b":"🧑‍🍼","k":["baby","feed","feeding","nanny","newborn","nursing","parent","person"],"s":true},{"b":"👼","k":["angel","baby","church","face","fairy","fairytale","fantasy","tale"],"s":true},{"b":"🎅","k":["celebration","christmas","claus","fairy","fantasy","father","holiday","merry","santa","tale","xmas"],"s":true},{"b":"🤶","k":["celebration","christmas","claus","fairy","fantasy","holiday","merry","mother","mrs","mrs.","santa","tale","xmas"],"s":true},{"b":"🧑‍🎄","k":["celebration","christmas","claus","fairy","fantasy","holiday","merry","mx","santa","tale","xmas"],"s":true},{"b":"🦸","k":["good","hero","superhero","superpower"],"s":true},{"b":"🦸‍♂️","k":["man","superhero"],"s":true},{"b":"🦸‍♀️","k":["superhero","woman"],"s":true},{"b":"🦹","k":["bad","criminal","evil","superpower","supervillain","villain"],"s":true},{"b":"🦹‍♂️","k":["man","supervillain"],"s":true},{"b":"🦹‍♀️","k":["supervillain","woman"],"s":true},{"b":"🧙","k":["fantasy","mage","magic","play","sorcerer","sorceress","sorcery","spell","summon","witch","wizard"],"s":true},{"b":"🧙‍♂️","k":["mage","man","wizard"],"s":true},{"b":"🧙‍♀️","k":["mage","wizard","woman"],"s":true},{"b":"🧚","k":["fairy","fairytale","fantasy","myth","person","pixie","tale","wings"],"s":true},{"b":"🧚‍♂️","k":["fairy","man"],"s":true},{"b":"🧚‍♀️","k":["fairy","woman"],"s":true},{"b":"🧛","k":["blood","dracula","fangs","halloween","scary","supernatural","teeth","undead","vampire"],"s":true},{"b":"🧛‍♂️","k":["man","vampire"],"s":true},{"b":"🧛‍♀️","k":["vampire","woman"],"s":true},{"b":"🧜","k":["creature","fairytale","folklore","merperson","ocean","sea","siren","trident"],"s":true},{"b":"🧜‍♂️","k":["merman"],"s":true},{"b":"🧜‍♀️","k":["mermaid"],"s":true},{"b":"🧝","k":["elf","elves","enchantment","fantasy","folklore","magic","magical","myth"],"s":true},{"b":"🧝‍♂️","k":["elf","man"],"s":true},{"b":"🧝‍♀️","k":["elf","woman"],"s":true},{"b":"🧞","k":["djinn","fantasy","genie","jinn","lamp","myth","rub","wishes"]},{"b":"🧞‍♂️","k":["genie","man"]},{"b":"🧞‍♀️","k":["genie","woman"]},{"b":"🧟","k":["apocalypse","dead","halloween","horror","scary","undead","walking","zombie"]},{"b":"🧟‍♂️","k":["man","zombie"]},{"b":"🧟‍♀️","k":["woman","zombie"]},{"b":"🧌","k":["fairy","fantasy","monster","tale","troll","trolling"]},{"b":"💆","k":["face","getting","headache","massage","person","relax","relaxing","salon","soothe","spa","tension","therapy","treatment"],"s":true},{"b":"💆‍♂️","k":["getting","man","massage","spa"],"s":true},{"b":"💆‍♀️","k":["getting","massage","spa","woman"],"s":true},{"b":"💇","k":["barber","beauty","chop","cosmetology","cut","getting","groom","hair","haircut","parlor","person","shears","style"],"s":true},{"b":"💇‍♂️","k":["getting","haircut","man"],"s":true},{"b":"💇‍♀️","k":["getting","haircut","woman"],"s":true},{"b":"🚶","k":["amble","gait","hike","man","pace","pedestrian","person","stride","stroll","walk","walking"],"s":true},{"b":"🚶‍♂️","k":["man","walking"],"s":true},{"b":"🚶‍♀️","k":["walking","woman"],"s":true},{"b":"🧍","k":["person","stand","standing"],"s":true},{"b":"🧍‍♂️","k":["man","standing"],"s":true},{"b":"🧍‍♀️","k":["standing","woman"],"s":true},{"b":"🧎","k":["kneel","kneeling","knees","person"],"s":true},{"b":"🧎‍♂️","k":["kneeling","man"],"s":true},{"b":"🧎‍♀️","k":["kneeling","woman"],"s":true},{"b":"🧑‍🦯","k":["accessibility","blind","cane","person","probing","white","with"],"s":true},{"b":"👨‍🦯","k":["accessibility","blind","cane","man","probing","white","with"],"s":true},{"b":"👩‍🦯","k":["accessibility","blind","cane","probing","white","with","woman"],"s":true},{"b":"🧑‍🦼","k":["accessibility","in","motorized","person","wheelchair"],"s":true},{"b":"👨‍🦼","k":["accessibility","in","man","motorized","wheelchair"],"s":true},{"b":"👩‍🦼","k":["accessibility","in","motorized","wheelchair","woman"],"s":true},{"b":"🧑‍🦽","k":["accessibility","in","manual","person","wheelchair"],"s":true},{"b":"👨‍🦽","k":["accessibility","in","man","manual","wheelchair"],"s":true},{"b":"👩‍🦽","k":["accessibility","in","manual","wheelchair","woman"],"s":true},{"b":"🏃","k":["exercise","fast","hurry","marathon","move","person","quick","race","racing","run","runner","running","rush","speed","workout"],"s":true},{"b":"🏃‍♂️","k":["exercise","man","marathon","running","workout"],"s":true},{"b":"🏃‍♀️","k":["exercise","marathon","running","woman","workout"],"s":true},{"b":"💃","k":["dance","dancer","dancing","dress","elegant","festive","flair","flamenco","groove","let’s","salsa","tango","woman"],"s":true},{"b":"🕺","k":["dance","dancer","dancing","elegant","festive","flair","flamenco","groove","let’s","man","salsa","tango"],"s":true},{"b":"🕴️","k":["business","levitating","person","suit"],"s":true},{"b":"👯","k":["bestie","bff","bunny","counterpart","dancer","dancers","double","ear","ears","identical","pair","party","partying","people","soulmate","twin","twinsies","with"]},{"b":"👯‍♂️","k":["bunny","dancing","ears","men","with"]},{"b":"👯‍♀️","k":["bunny","dancing","ears","with","women"]},{"b":"🧖","k":["day","luxurious","pamper","person","relax","room","sauna","spa","steam","steambath","steamy","unwind"],"s":true},{"b":"🧖‍♂️","k":["man","room","sauna","steamy"],"s":true},{"b":"🧖‍♀️","k":["room","sauna","steamy","woman"],"s":true},{"b":"🧗","k":["bouldering","climb","climber","climbing","mountain","person","rock","scale","up"],"s":true},{"b":"🧗‍♂️","k":["bouldering","climbing","man"],"s":true},{"b":"🧗‍♀️","k":["bouldering","climbing","woman"],"s":true},{"b":"🤺","k":["fencer","fencing","person","sword"]},{"b":"🏇","k":["horse","jockey","racehorse","racing","riding","sport"],"s":true},{"b":"⛷️","k":["skier"]},{"b":"🏂","k":["ski","snow","snowboard","snowboarder","sport"],"s":true},{"b":"🏌️","k":["golfing","person"],"s":true},{"b":"🏌️‍♂️","k":["golfing","man"],"s":true},{"b":"🏌️‍♀️","k":["golfing","woman"],"s":true},{"b":"🏄","k":["beach","ocean","person","sport","surf","surfer","surfing","swell","waves"],"s":true},{"b":"🏄‍♂️","k":["man","surfing"],"s":true},{"b":"🏄‍♀️","k":["surfing","woman"],"s":true},{"b":"🚣","k":["boat","canoe","cruise","fishing","lake","oar","paddle","person","raft","river","row","rowboat","rowing"],"s":true},{"b":"🚣‍♂️","k":["boat","man","rowing"],"s":true},{"b":"🚣‍♀️","k":["boat","rowing","woman"],"s":true},{"b":"🏊","k":["freestyle","person","sport","swim","swimmer","swimming","triathlon"],"s":true},{"b":"🏊‍♂️","k":["man","swimming"],"s":true},{"b":"🏊‍♀️","k":["swimming","woman"],"s":true},{"b":"⛹️","k":["ball","basketball","bouncing","person"],"s":true},{"b":"⛹️‍♂️","k":["ball","basketball","bouncing","man"],"s":true},{"b":"⛹️‍♀️","k":["ball","basketball","bouncing","woman"],"s":true},{"b":"🏋️","k":["gym","lifting","person","weight","weights","workout"],"s":true},{"b":"🏋️‍♂️","k":["gym","lifting","man","weight","weights","workout"],"s":true},{"b":"🏋️‍♀️","k":["gym","lifting","weight","weights","woman","workout"],"s":true},{"b":"🚴","k":["bicycle","bicyclist","bike","biking","cycle","cyclist","person","riding","sport"],"s":true},{"b":"🚴‍♂️","k":["biking","man"],"s":true},{"b":"🚴‍♀️","k":["biking","woman"],"s":true},{"b":"🚵","k":["bicycle","bicyclist","bike","biking","cycle","cyclist","mountain","person","riding","sport"],"s":true},{"b":"🚵‍♂️","k":["biking","man","mountain"],"s":true},{"b":"🚵‍♀️","k":["biking","mountain","woman"],"s":true},{"b":"🤸","k":["active","cartwheel","cartwheeling","excited","flip","gymnastics","happy","person","somersault"],"s":true},{"b":"🤸‍♂️","k":["cartwheeling","man"],"s":true},{"b":"🤸‍♀️","k":["cartwheeling","woman"],"s":true},{"b":"🤼","k":["combat","duel","grapple","people","ring","tournament","wrestle","wrestling"]},{"b":"🤼‍♂️","k":["men","wrestling"]},{"b":"🤼‍♀️","k":["women","wrestling"]},{"b":"🤽","k":["person","playing","polo","sport","swimming","water","waterpolo"],"s":true},{"b":"🤽‍♂️","k":["man","playing","polo","water"],"s":true},{"b":"🤽‍♀️","k":["playing","polo","water","woman"],"s":true},{"b":"🤾","k":["athletics","ball","catch","chuck","handball","hurl","lob","person","pitch","playing","sport","throw","toss"],"s":true},{"b":"🤾‍♂️","k":["handball","man","playing"],"s":true},{"b":"🤾‍♀️","k":["handball","playing","woman"],"s":true},{"b":"🤹","k":["act","balance","balancing","handle","juggle","juggling","manage","multitask","person","skill"],"s":true},{"b":"🤹‍♂️","k":["juggling","man"],"s":true},{"b":"🤹‍♀️","k":["juggling","woman"],"s":true},{"b":"🧘","k":["cross","legged","legs","lotus","meditation","peace","person","position","relax","serenity","yoga","yogi","zen"],"s":true},{"b":"🧘‍♂️","k":["lotus","man","meditation","position"],"s":true},{"b":"🧘‍♀️","k":["lotus","meditation","position","woman"],"s":true},{"b":"🛀","k":["bath","bathtub","person","shower","taking","tub"],"s":true},{"b":"🛌","k":["bed","bedtime","good","goodnight","hotel","nap","night","person","sleep","sleeping","tired","zzz"],"s":true},{"b":"🧑‍🤝‍🧑","k":["bae","bestie","bff","couple","date","dating","flirt","friends","hand","hands","hold","holding","people","twins"],"s":true},{"b":"👭","k":["bae","bestie","bff","couple","date","dating","flirt","friends","girls","hand","hands","hold","holding","sisters","twins","two","women"],"s":true},{"b":"👫","k":["and","bae","bestie","bff","couple","date","dating","flirt","friends","hand","hands","hold","holding","man","twins","woman"],"s":true},{"b":"👬","k":["bae","bestie","bff","boys","brothers","couple","date","dating","flirt","friends","hand","hands","hold","holding","men","twins","two"],"s":true},{"b":"💏","k":["anniversary","babe","bae","couple","couplekiss","date","dating","heart","kiss","love","mwah","person","romance","together","xoxo"],"s":true},{"b":"👩‍❤️‍💋‍👨","k":["couplekiss","kiss:","man","woman","woman,"],"s":true},{"b":"👨‍❤️‍💋‍👨","k":["couplekiss","kiss:","man","man,"],"s":true},{"b":"👩‍❤️‍💋‍👩","k":["couplekiss","kiss:","woman","woman,"],"s":true},{"b":"💑","k":["anniversary","babe","bae","couple","dating","heart","kiss","love","person","relationship","romance","together","with","you"],"s":true},{"b":"👩‍❤️‍👨","k":["couple","heart","heart:","man","with","woman","woman,"],"s":true},{"b":"👨‍❤️‍👨","k":["couple","heart","heart:","man","man,","with"],"s":true},{"b":"👩‍❤️‍👩","k":["couple","heart","heart:","with","woman","woman,"],"s":true},{"b":"👪","k":["child","family","home","parents"]},{"b":"👨‍👩‍👦","k":["boy","family","family:","man","man,","woman","woman,"]},{"b":"👨‍👩‍👧","k":["family","family:","girl","man","man,","woman","woman,"]},{"b":"👨‍👩‍👧‍👦","k":["boy","family","family:","girl","girl,","man","man,","woman","woman,"]},{"b":"👨‍👩‍👦‍👦","k":["boy","boy,","family","family:","man","man,","woman","woman,"]},{"b":"👨‍👩‍👧‍👧","k":["family","family:","girl","girl,","man","man,","woman","woman,"]},{"b":"👨‍👨‍👦","k":["boy","family","family:","man","man,"]},{"b":"👨‍👨‍👧","k":["family","family:","girl","man","man,"]},{"b":"👨‍👨‍👧‍👦","k":["boy","family","family:","girl","girl,","man","man,"]},{"b":"👨‍👨‍👦‍👦","k":["boy","boy,","family","family:","man","man,"]},{"b":"👨‍👨‍👧‍👧","k":["family","family:","girl","girl,","man","man,"]},{"b":"👩‍👩‍👦","k":["boy","family","family:","woman","woman,"]},{"b":"👩‍👩‍👧","k":["family","family:","girl","woman","woman,"]},{"b":"👩‍👩‍👧‍👦","k":["boy","family","family:","girl","girl,","woman","woman,"]},{"b":"👩‍👩‍👦‍👦","k":["boy","boy,","family","family:","woman","woman,"]},{"b":"👩‍👩‍👧‍👧","k":["family","family:","girl","girl,","woman","woman,"]},{"b":"👨‍👦","k":["boy","family","family:","man","man,"]},{"b":"👨‍👦‍👦","k":["boy","boy,","family","family:","man","man,"]},{"b":"👨‍👧","k":["family","family:","girl","man","man,"]},{"b":"👨‍👧‍👦","k":["boy","family","family:","girl","girl,","man","man,"]},{"b":"👨‍👧‍👧","k":["family","family:","girl","girl,","man","man,"]},{"b":"👩‍👦","k":["boy","family","family:","woman","woman,"]},{"b":"👩‍👦‍👦","k":["boy","boy,","family","family:","woman","woman,"]},{"b":"👩‍👧","k":["family","family:","girl","woman","woman,"]},{"b":"👩‍👧‍👦","k":["boy","family","family:","girl","girl,","woman","woman,"]},{"b":"👩‍👧‍👧","k":["family","family:","girl","girl,","woman","woman,"]},{"b":"🗣️","k":["head","speaking"]},{"b":"👤","k":["bust","in","mysterious","shadow","silhouette","user"]},{"b":"👥","k":["bff","bust","busts","everyone","friend","friends","group","in","people","silhouette","team","users"]},{"b":"🫂","k":["comfort","embrace","farewell","friendship","goodbye","hello","hug","hugging","love","people","thanks"]},{"b":"👣","k":["barefoot","clothing","feet","footprint","footprints","omw","print","tracks","walk"]}]},{"name":"Animals & Nature","icon":"🐾","emojis":[{"b":"🐵","k":["animal","banana","face","monkey"]},{"b":"🐒","k":["animal","banana","monkey"]},{"b":"🦍","k":["animal","gorilla"]},{"b":"🦧","k":["animal","ape","monkey","orangutan"]},{"b":"🐶","k":["adorbs","animal","dog","face","pet","puppies","puppy"]},{"b":"🐕","k":["animal","animals","dog","dog2","dogs","pet"]},{"b":"🦮","k":["accessibility","animal","blind","dog","guide"]},{"b":"🐕‍🦺","k":["accessibility","animal","assistance","dog","service"]},{"b":"🐩","k":["animal","dog","fluffy","poodle"]},{"b":"🐺","k":["animal","face","wolf"]},{"b":"🦊","k":["animal","face","fox"]},{"b":"🦝","k":["animal","curious","raccoon","sly"]},{"b":"🐱","k":["animal","cat","face","kitten","kitty","pet"]},{"b":"🐈","k":["animal","animals","cat","cat2","cats","kitten","pet"]},{"b":"🐈‍⬛","k":["animal","black","cat","feline","halloween","meow","unlucky"]},{"b":"🦁","k":["alpha","animal","face","leo","lion","mane","order","rawr","roar","safari","strong","zodiac"]},{"b":"🐯","k":["animal","big","cat","face","predator","tiger"]},{"b":"🐅","k":["animal","big","cat","predator","tiger","tiger2","zoo"]},{"b":"🐆","k":["animal","big","cat","leopard","predator","zoo"]},{"b":"🐴","k":["animal","dressage","equine","face","farm","horse","horses"]},{"b":"🫎","k":["alces","animal","antlers","canada","elk","mammal","moose"]},{"b":"🫏","k":["animal","ass","burro","donkey","hinny","mammal","mule","stubborn"]},{"b":"🐎","k":["animal","equestrian","farm","horse","racehorse","racing","speed"]},{"b":"🦄","k":["face","unicorn"]},{"b":"🦓","k":["animal","stripe","zebra"]},{"b":"🦌","k":["animal","deer"]},{"b":"🦬","k":["animal","bison","buffalo","herd","wisent"]},{"b":"🐮","k":["animal","cow","face","farm","milk","moo"]},{"b":"🐂","k":["animal","animals","bull","farm","ox","taurus","zodiac"]},{"b":"🐃","k":["animal","buffalo","water","zoo"]},{"b":"🐄","k":["animal","animals","cow","cow2","farm","milk","moo"]},{"b":"🐷","k":["animal","bacon","face","farm","pig","pork"]},{"b":"🐖","k":["animal","bacon","farm","pig","pig2","pork","sow"]},{"b":"🐗","k":["animal","boar","pig"]},{"b":"🐽","k":["animal","face","farm","nose","pig","smell","snout"]},{"b":"🐏","k":["animal","aries","horns","male","ram","sheep","zodiac","zoo"]},{"b":"🐑","k":["animal","baa","ewe","farm","female","fluffy","lamb","sheep","wool"]},{"b":"🐐","k":["animal","capricorn","farm","goat","milk","zodiac"]},{"b":"🐪","k":["animal","camel","desert","dromedary","hump","one"]},{"b":"🐫","k":["animal","bactrian","camel","desert","hump","two","two-hump"]},{"b":"🦙","k":["alpaca","animal","guanaco","llama","vicuña","wool"]},{"b":"🦒","k":["animal","giraffe","spots"]},{"b":"🐘","k":["animal","elephant"]},{"b":"🦣","k":["animal","extinction","large","mammoth","tusk","wooly"]},{"b":"🦏","k":["animal","rhinoceros"]},{"b":"🦛","k":["animal","hippo","hippopotamus"]},{"b":"🐭","k":["animal","face","mouse"]},{"b":"🐁","k":["animal","animals","mouse","mouse2"]},{"b":"🐀","k":["animal","rat"]},{"b":"🐹","k":["animal","face","hamster","pet"]},{"b":"🐰","k":["animal","bunny","face","pet","rabbit"]},{"b":"🐇","k":["animal","bunny","pet","rabbit","rabbit2"]},{"b":"🐿️","k":["chipmunk"]},{"b":"🦫","k":["animal","beaver","dam","teeth"]},{"b":"🦔","k":["animal","hedgehog","spiny"]},{"b":"🦇","k":["animal","bat","vampire"]},{"b":"🐻","k":["animal","bear","face","grizzly","growl","honey"]},{"b":"🐻‍❄️","k":["bear","polar"]},{"b":"🐨","k":["animal","australia","bear","down","face","koala","marsupial","under"]},{"b":"🐼","k":["animal","bamboo","face","panda"]},{"b":"🦥","k":["lazy","sloth","slow"]},{"b":"🦦","k":["animal","fishing","otter","playful"]},{"b":"🦨","k":["animal","skunk","stink"]},{"b":"🦘","k":["animal","joey","jump","kangaroo","marsupial"]},{"b":"🦡","k":["animal","badger","honey","pester"]},{"b":"🐾","k":["feet","paw","paws","print","prints"]},{"b":"🦃","k":["bird","gobble","thanksgiving","turkey"]},{"b":"🐔","k":["animal","bird","chicken","ornithology"]},{"b":"🐓","k":["animal","bird","ornithology","rooster"]},{"b":"🐣","k":["animal","baby","bird","chick","egg","hatching"]},{"b":"🐤","k":["animal","baby","bird","chick","ornithology"]},{"b":"🐥","k":["animal","baby","bird","chick","front-facing","hatched","newborn","ornithology"]},{"b":"🐦","k":["animal","bird","ornithology"]},{"b":"🐧","k":["animal","antarctica","bird","ornithology","penguin"]},{"b":"🕊️","k":["dove","peace"]},{"b":"🦅","k":["animal","bird","eagle","ornithology"]},{"b":"🦆","k":["animal","bird","duck","ornithology"]},{"b":"🦢","k":["animal","bird","cygnet","duckling","ornithology","swan","ugly"]},{"b":"🦉","k":["animal","bird","ornithology","owl","wise"]},{"b":"🦤","k":["animal","bird","dodo","extinction","large","ornithology"]},{"b":"🪶","k":["bird","feather","flight","light","plumage"]},{"b":"🦩","k":["animal","bird","flamboyant","flamingo","ornithology","tropical"]},{"b":"🦚","k":["animal","bird","colorful","ornithology","ostentatious","peacock","peahen","pretty","proud"]},{"b":"🦜","k":["animal","bird","ornithology","parrot","pirate","talk"]},{"b":"🪽","k":["angelic","ascend","aviation","bird","fly","flying","heavenly","mythology","soar","wing"]},{"b":"🐦‍⬛","k":["animal","beak","bird","black","caw","corvid","crow","ornithology","raven","rook"]},{"b":"🪿","k":["animal","bird","duck","flock","fowl","gaggle","gander","geese","goose","honk","ornithology","silly"]},{"b":"🐸","k":["animal","face","frog"]},{"b":"🐊","k":["animal","crocodile","zoo"]},{"b":"🐢","k":["animal","slow","terrapin","tortoise","turtle"]},{"b":"🦎","k":["animal","lizard","reptile"]},{"b":"🐍","k":["animal","bearer","ophiuchus","serpent","snake","zodiac"]},{"b":"🐲","k":["animal","dragon","face","fairy","fairytale","tale"]},{"b":"🐉","k":["animal","dragon","fairy","fairytale","knights","tale"]},{"b":"🦕","k":["brachiosaurus","brontosaurus","dinosaur","diplodocus","sauropod"]},{"b":"🦖","k":["dinosaur","rex","t","t-rex","tyrannosaurus"]},{"b":"🐳","k":["animal","beach","face","ocean","sea","spouting","whale"]},{"b":"🐋","k":["animal","beach","ocean","whale","whale2"]},{"b":"🐬","k":["animal","beach","dolphin","flipper","ocean"]},{"b":"🦭","k":["animal","lion","ocean","sea","seal"]},{"b":"🐟","k":["animal","dinner","fish","fishes","fishing","pisces","zodiac"]},{"b":"🐠","k":["animal","fish","fishes","tropical"]},{"b":"🐡","k":["animal","blowfish","fish"]},{"b":"🦈","k":["animal","fish","shark"]},{"b":"🐙","k":["animal","creature","ocean","octopus"]},{"b":"🐚","k":["animal","beach","conch","sea","shell","spiral"]},{"b":"🪸","k":["change","climate","coral","ocean","reef","sea"]},{"b":"🪼","k":["animal","aquarium","burn","invertebrate","jelly","jellyfish","life","marine","ocean","ouch","plankton","sea","sting","stinger","tentacles"]},{"b":"🐌","k":["animal","escargot","garden","nature","slow","slug","snail"]},{"b":"🦋","k":["butterfly","insect","pretty"]},{"b":"🐛","k":["animal","bug","garden","insect"]},{"b":"🐜","k":["animal","ant","garden","insect"]},{"b":"🐝","k":["animal","bee","bumblebee","honey","honeybee","insect","nature","spring"]},{"b":"🪲","k":["animal","beetle","bug","insect"]},{"b":"🐞","k":["animal","beetle","bug","garden","insect","lady","ladybird","ladybug","nature"]},{"b":"🦗","k":["animal","bug","cricket","grasshopper","insect","orthoptera"]},{"b":"🪳","k":["animal","cockroach","insect","pest","roach"]},{"b":"🕷️","k":["spider"]},{"b":"🕸️","k":["spider","web"]},{"b":"🦂","k":["scorpio","scorpion","scorpius","zodiac"]},{"b":"🦟","k":["bite","disease","fever","insect","malaria","mosquito","pest","virus"]},{"b":"🪰","k":["animal","disease","fly","insect","maggot","pest","rotting"]},{"b":"🪱","k":["animal","annelid","earthworm","parasite","worm"]},{"b":"🦠","k":["amoeba","bacteria","germ","microbe","science","virus"]},{"b":"💐","k":["anniversary","birthday","bouquet","date","flower","flowers","love","plant","romance"]},{"b":"🌸","k":["blossom","cherry","flower","plant","spring","springtime"]},{"b":"💮","k":["flower","white"]},{"b":"🪷","k":["beauty","buddhism","calm","flower","hinduism","lotus","peace","purity","serenity"]},{"b":"🏵️","k":["rosette"]},{"b":"🌹","k":["beauty","elegant","flower","love","plant","red","rose","valentine"]},{"b":"🥀","k":["dying","flower","wilted"]},{"b":"🌺","k":["flower","hibiscus","plant"]},{"b":"🌻","k":["flower","outdoors","plant","sun","sunflower"]},{"b":"🌼","k":["blossom","buttercup","dandelion","flower","plant"]},{"b":"🌷","k":["blossom","flower","growth","plant","tulip"]},{"b":"🪻","k":["bloom","bluebonnet","flower","hyacinth","indigo","lavender","lilac","lupine","plant","purple","shrub","snapdragon","spring","violet"]},{"b":"🌱","k":["plant","sapling","seedling","sprout","young"]},{"b":"🪴","k":["decor","grow","house","nurturing","plant","pot","potted"]},{"b":"🌲","k":["christmas","evergreen","forest","pine","tree","wood"]},{"b":"🌳","k":["deciduous","forest","green","habitat","shedding","tree","wood"]},{"b":"🌴","k":["beach","palm","plant","tree","tropical"]},{"b":"🌵","k":["cactus","desert","drought","nature","plant"]},{"b":"🌾","k":["ear","grain","grains","of","plant","rice","sheaf"]},{"b":"🌿","k":["herb","leaf","plant"]},{"b":"☘️","k":["shamrock"]},{"b":"🍀","k":["4","clover","four","four-leaf","irish","leaf","luck","lucky","plant"]},{"b":"🍁","k":["canada","falling","leaf","maple"]},{"b":"🍂","k":["autumn","fall","fallen","falling","leaf"]},{"b":"🍃","k":["blow","flutter","fluttering","leaf","leaves","wind"]},{"b":"🪹","k":["branch","empty","home","nest","nesting"]},{"b":"🪺","k":["bird","branch","egg","eggs","nest","nesting","with"]},{"b":"🍄","k":["fungus","mushroom","toadstool"]}]},{"name":"Food & Drink","icon":"🍔","emojis":[{"b":"🍇","k":["dionysus","fruit","grape","grapes"]},{"b":"🍈","k":["cantaloupe","fruit","melon"]},{"b":"🍉","k":["fruit","watermelon"]},{"b":"🍊","k":["c","citrus","fruit","mandarin","nectarine","orange","tangerine","vitamin"]},{"b":"🍋","k":["citrus","fruit","lemon","sour"]},{"b":"🍌","k":["banana","fruit","potassium"]},{"b":"🍍","k":["colada","fruit","pina","pineapple","tropical"]},{"b":"🥭","k":["food","fruit","mango","tropical"]},{"b":"🍎","k":["apple","diet","food","fruit","health","red","ripe"]},{"b":"🍏","k":["apple","fruit","green"]},{"b":"🍐","k":["fruit","pear"]},{"b":"🍑","k":["fruit","peach"]},{"b":"🍒","k":["berries","cherries","cherry","fruit","red"]},{"b":"🍓","k":["berry","fruit","strawberry"]},{"b":"🫐","k":["berries","berry","bilberry","blue","blueberries","blueberry","food","fruit"]},{"b":"🥝","k":["food","fruit","kiwi"]},{"b":"🍅","k":["food","fruit","tomato","vegetable"]},{"b":"🫒","k":["food","olive"]},{"b":"🥥","k":["coconut","colada","palm","piña"]},{"b":"🥑","k":["avocado","food","fruit"]},{"b":"🍆","k":["aubergine","eggplant","vegetable"]},{"b":"🥔","k":["food","potato","vegetable"]},{"b":"🥕","k":["carrot","food","vegetable"]},{"b":"🌽","k":["corn","crops","ear","farm","maize","maze"]},{"b":"🌶️","k":["hot","pepper","spicy"]},{"b":"🫑","k":["bell","capsicum","food","pepper","vegetable"]},{"b":"🥒","k":["cucumber","food","pickle","vegetable"]},{"b":"🥬","k":["bok","burgers","cabbage","choy","green","kale","leafy","lettuce","salad"]},{"b":"🥦","k":["broccoli","cabbage","wild"]},{"b":"🧄","k":["flavoring","garlic"]},{"b":"🧅","k":["flavoring","onion"]},{"b":"🥜","k":["food","nut","peanut","peanuts","vegetable"]},{"b":"🫘","k":["beans","food","kidney","legume","small"]},{"b":"🌰","k":["almond","chestnut","plant"]},{"b":"🫚","k":["beer","ginger","health","herb","natural","root","spice"]},{"b":"🫛","k":["beans","beanstalk","edamame","legume","pea","pod","soybean","vegetable","veggie"]},{"b":"🍞","k":["bread","carbs","food","grain","loaf","restaurant","toast","wheat"]},{"b":"🥐","k":["bread","breakfast","crescent","croissant","food","french","roll"]},{"b":"🥖","k":["baguette","bread","food","french"]},{"b":"🫓","k":["arepa","bread","flatbread","food","gordita","lavash","naan","pita"]},{"b":"🥨","k":["convoluted","pretzel","twisted"]},{"b":"🥯","k":["bagel","bakery","bread","breakfast","schmear"]},{"b":"🥞","k":["breakfast","crêpe","food","hotcake","pancake","pancakes"]},{"b":"🧇","k":["breakfast","indecisive","iron","waffle"]},{"b":"🧀","k":["cheese","wedge"]},{"b":"🍖","k":["bone","meat","on"]},{"b":"🍗","k":["bone","chicken","drumstick","hungry","leg","meat","poultry","turkey"]},{"b":"🥩","k":["chop","cut","lambchop","meat","of","porkchop","red","steak"]},{"b":"🥓","k":["bacon","breakfast","food","meat"]},{"b":"🍔","k":["burger","eat","fast","food","hamburger","hungry"]},{"b":"🍟","k":["fast","food","french","fries"]},{"b":"🍕","k":["cheese","food","hungry","pepperoni","pizza","slice"]},{"b":"🌭","k":["dog","frankfurter","hot","hotdog","sausage"]},{"b":"🥪","k":["bread","sandwich"]},{"b":"🌮","k":["mexican","taco"]},{"b":"🌯","k":["burrito","mexican","wrap"]},{"b":"🫔","k":["food","mexican","pamonha","tamale","wrapped"]},{"b":"🥙","k":["falafel","flatbread","food","gyro","kebab","stuffed"]},{"b":"🧆","k":["chickpea","falafel","meatball"]},{"b":"🥚","k":["breakfast","egg","food"]},{"b":"🍳","k":["breakfast","cooking","easy","egg","fried","fry","frying","over","pan","restaurant","side","sunny","up"]},{"b":"🥘","k":["casserole","curry","food","of","paella","pan","shallow"]},{"b":"🍲","k":["food","pot","soup","stew"]},{"b":"🫕","k":["cheese","chocolate","fondue","food","melted","pot","ski"]},{"b":"🥣","k":["bowl","breakfast","cereal","congee","oatmeal","porridge","spoon","with"]},{"b":"🥗","k":["food","green","salad"]},{"b":"🍿","k":["corn","movie","pop","popcorn"]},{"b":"🧈","k":["butter","dairy"]},{"b":"🧂","k":["condiment","flavor","mad","salt","salty","shaker","taste","upset"]},{"b":"🥫","k":["can","canned","food"]},{"b":"🍱","k":["bento","box","food"]},{"b":"🍘","k":["cracker","food","rice"]},{"b":"🍙","k":["ball","food","japanese","rice"]},{"b":"🍚","k":["cooked","food","rice"]},{"b":"🍛","k":["curry","food","rice"]},{"b":"🍜","k":["bowl","chopsticks","food","noodle","pho","ramen","soup","steaming"]},{"b":"🍝","k":["food","meatballs","pasta","restaurant","spaghetti"]},{"b":"🍠","k":["food","potato","roasted","sweet"]},{"b":"🍢","k":["food","kebab","oden","restaurant","seafood","skewer","stick"]},{"b":"🍣","k":["food","sushi"]},{"b":"🍤","k":["fried","prawn","shrimp","tempura"]},{"b":"🍥","k":["cake","fish","food","pastry","restaurant","swirl","with"]},{"b":"🥮","k":["autumn","cake","festival","moon","yuèbǐng"]},{"b":"🍡","k":["dango","dessert","japanese","skewer","stick","sweet"]},{"b":"🥟","k":["dumpling","empanada","gyōza","jiaozi","pierogi","potsticker"]},{"b":"🥠","k":["cookie","fortune","prophecy"]},{"b":"🥡","k":["box","chopsticks","delivery","food","oyster","pail","takeout"]},{"b":"🦀","k":["cancer","crab","zodiac"]},{"b":"🦞","k":["animal","bisque","claws","lobster","seafood"]},{"b":"🦐","k":["food","shellfish","shrimp","small"]},{"b":"🦑","k":["animal","food","mollusk","squid"]},{"b":"🦪","k":["diving","oyster","pearl"]},{"b":"🍦","k":["cream","dessert","food","ice","icecream","restaurant","serve","soft","sweet"]},{"b":"🍧","k":["dessert","ice","restaurant","shaved","sweet"]},{"b":"🍨","k":["cream","dessert","food","ice","restaurant","sweet"]},{"b":"🍩","k":["breakfast","dessert","donut","doughnut","food","sweet"]},{"b":"🍪","k":["chip","chocolate","cookie","dessert","sweet"]},{"b":"🎂","k":["bday","birthday","cake","celebration","dessert","happy","party","pastry","sweet"]},{"b":"🍰","k":["cake","dessert","pastry","shortcake","slice","sweet"]},{"b":"🧁","k":["bakery","cupcake","dessert","sprinkles","sugar","sweet","treat"]},{"b":"🥧","k":["apple","filling","fruit","meat","pastry","pie","pumpkin","slice"]},{"b":"🍫","k":["bar","candy","chocolate","dessert","halloween","sweet","tooth"]},{"b":"🍬","k":["candy","cavities","dessert","halloween","restaurant","sweet","tooth","wrapper"]},{"b":"🍭","k":["candy","dessert","food","lollipop","restaurant","sweet"]},{"b":"🍮","k":["custard","dessert","pudding","sweet"]},{"b":"🍯","k":["barrel","bear","food","honey","honeypot","jar","pot","sweet"]},{"b":"🍼","k":["babies","baby","birth","born","bottle","drink","infant","milk","newborn"]},{"b":"🥛","k":["drink","glass","milk"]},{"b":"☕","k":["beverage","cafe","caffeine","chai","coffee","drink","espresso","hot","morning","steaming","tea"]},{"b":"🫖","k":["brew","drink","food","pot","tea","teapot"]},{"b":"🍵","k":["beverage","breakfast","cup","drink","green","handle","oolong","tea","teacup","without"]},{"b":"🍶","k":["bar","beverage","bottle","cup","drink","restaurant","sake"]},{"b":"🍾","k":["bar","bottle","bubbly","celebration","champagne","cork","drink","popping","with"]},{"b":"🍷","k":["alcohol","bar","beverage","booze","club","drink","drinking","drinks","glass","restaurant","wine"]},{"b":"🍸","k":["alcohol","bar","booze","club","cocktail","drink","drinking","drinks","glass","mad","martini","men"]},{"b":"🍹","k":["alcohol","bar","booze","club","cocktail","drink","drinking","drinks","drunk","mai","party","summer","tai","tropical","tropics","vacation"]},{"b":"🍺","k":["alcohol","ale","bar","beer","booze","drink","drinking","drinks","mug","octoberfest","oktoberfest","pint","stein","summer"]},{"b":"🍻","k":["alcohol","bar","beer","beers","booze","bottoms","cheers","clink","clinking","drinking","drinks","mugs"]},{"b":"🥂","k":["celebrate","cheers","clink","clinking","drink","glass","glasses","toast"]},{"b":"🥃","k":["glass","liquor","scotch","shot","tumbler","whiskey","whisky"]},{"b":"🫗","k":["accident","drink","empty","glass","liquid","oops","pour","pouring","spill","water"]},{"b":"🥤","k":["cup","drink","juice","malt","soda","soft","straw","water","with"]},{"b":"🧋","k":["boba","bubble","food","milk","pearl","tea"]},{"b":"🧃","k":["beverage","box","juice","straw","sweet"]},{"b":"🧉","k":["drink","mate"]},{"b":"🧊","k":["cold","cube","ice","iceberg"]},{"b":"🥢","k":["chopsticks","hashi","jeotgarak","kuaizi"]},{"b":"🍽️","k":["and","cutlery","dining","dinner","fork","knife","plate","with"]},{"b":"🍴","k":["and","breakfast","breaky","cooking","cutlery","delicious","dinner","eat","feed","food","fork","hungry","knife","lunch","restaurant","yum","yummy"]},{"b":"🥄","k":["eat","spoon","tableware"]},{"b":"🔪","k":["chef","chop","cooking","cut","hocho","kitchen","knife","tool","weapon"]},{"b":"🫙","k":["condiment","container","empty","jar","nothing","sauce","store"]},{"b":"🏺","k":["amphora","aquarius","cooking","drink","jug","tool","weapon","zodiac"]}]},{"name":"Travel & Places","icon":"✈️","emojis":[{"b":"🌍","k":["africa","earth","europe","europe-africa","globe","international","showing","world"]},{"b":"🌎","k":["americas","earth","globe","international","showing","world"]},{"b":"🌏","k":["asia","asia-australia","australia","earth","globe","international","showing","world"]},{"b":"🌐","k":["earth","global","globe","international","internet","meridians","web","with","world","worldwide"]},{"b":"🗺️","k":["map","travel","world"]},{"b":"🗾","k":["japan","map"]},{"b":"🧭","k":["compass","direction","magnetic","navigation","orienteering"]},{"b":"🏔️","k":["mountain","snow","snow-capped"]},{"b":"⛰️","k":["mountain"]},{"b":"🌋","k":["eruption","mountain","nature","volcano"]},{"b":"🗻","k":["fuji","mount","mountain","nature"]},{"b":"🏕️","k":["camping"]},{"b":"🏖️","k":["beach","umbrella","with"]},{"b":"🏜️","k":["desert"]},{"b":"🏝️","k":["desert","island"]},{"b":"🏞️","k":["national","park"]},{"b":"🏟️","k":["stadium"]},{"b":"🏛️","k":["building","classical"]},{"b":"🏗️","k":["building","construction"]},{"b":"🧱","k":["brick","bricks","clay","mortar","wall"]},{"b":"🪨","k":["boulder","heavy","rock","solid","stone","tough"]},{"b":"🪵","k":["log","lumber","timber","wood"]},{"b":"🛖","k":["home","house","hut","roundhouse","shelter","yurt"]},{"b":"🏘️","k":["houses"]},{"b":"🏚️","k":["derelict","house"]},{"b":"🏠","k":["building","country","heart","home","house","ranch","settle","simple","suburban","suburbia","where"]},{"b":"🏡","k":["building","country","garden","heart","home","house","ranch","settle","simple","suburban","suburbia","where","with"]},{"b":"🏢","k":["building","city","cubical","job","office"]},{"b":"🏣","k":["building","japanese","office","post"]},{"b":"🏤","k":["building","european","office","post"]},{"b":"🏥","k":["building","doctor","hospital","medicine"]},{"b":"🏦","k":["bank","building"]},{"b":"🏨","k":["building","hotel"]},{"b":"🏩","k":["building","hotel","love"]},{"b":"🏪","k":["24","building","convenience","hours","store"]},{"b":"🏫","k":["building","school"]},{"b":"🏬","k":["building","department","store"]},{"b":"🏭","k":["building","factory"]},{"b":"🏯","k":["building","castle","japanese"]},{"b":"🏰","k":["building","castle","european"]},{"b":"💒","k":["chapel","hitched","marriage","nuptials","romance","wedding"]},{"b":"🗼","k":["tokyo","tower"]},{"b":"🗽","k":["liberty","new","ny","nyc","of","statue","york"]},{"b":"⛪","k":["bless","chapel","christian","church","cross","religion"]},{"b":"🕌","k":["islam","masjid","mosque","muslim","religion"]},{"b":"🛕","k":["hindu","temple"]},{"b":"🕍","k":["jew","jewish","judaism","religion","synagogue","temple"]},{"b":"⛩️","k":["shinto","shrine"]},{"b":"🕋","k":["hajj","islam","kaaba","muslim","religion","umrah"]},{"b":"⛲","k":["fountain"]},{"b":"⛺","k":["camping","tent"]},{"b":"🌁","k":["fog","foggy","karl"]},{"b":"🌃","k":["night","star","stars","with"]},{"b":"🏙️","k":["cityscape","skyline"]},{"b":"🌄","k":["morning","mountains","over","sun","sunrise"]},{"b":"🌅","k":["morning","nature","sun","sunrise"]},{"b":"🌆","k":["at","building","city","cityscape","dusk","evening","landscape","sun","sunset"]},{"b":"🌇","k":["building","city","dusk","sun","sunrise","sunset"]},{"b":"🌉","k":["at","bridge","night"]},{"b":"♨️","k":["hot","hotsprings","springs"]},{"b":"🎠","k":["carousel","entertainment","horse"]},{"b":"🛝","k":["amusement","park","play","playground","playing","slide","sliding","theme"]},{"b":"🎡","k":["amusement","ferris","park","theme","wheel"]},{"b":"🎢","k":["amusement","coaster","park","roller","theme"]},{"b":"💈","k":["barber","cut","fresh","haircut","pole","shave"]},{"b":"🎪","k":["circus","tent"]},{"b":"🚂","k":["caboose","engine","locomotive","railway","steam","train","trains","travel"]},{"b":"🚃","k":["car","electric","railway","train","tram","travel","trolleybus"]},{"b":"🚄","k":["bullettrain","high-speed","railway","shinkansen","side","speed","train"]},{"b":"🚅","k":["bullet","bullettrain","front","high-speed","nose","railway","shinkansen","speed","train","travel"]},{"b":"🚆","k":["arrived","choo","railway","train","train2"]},{"b":"🚇","k":["metro","subway","travel"]},{"b":"🚈","k":["arrived","light","monorail","rail","railway"]},{"b":"🚉","k":["railway","station","train"]},{"b":"🚊","k":["tram","trolleybus"]},{"b":"🚝","k":["monorail","vehicle"]},{"b":"🚞","k":["car","mountain","railway","trip"]},{"b":"🚋","k":["bus","car","train","tram","trolley","trolleybus"]},{"b":"🚌","k":["bus","school","vehicle"]},{"b":"🚍","k":["bus","cars","oncoming"]},{"b":"🚎","k":["bus","tram","trolley","trolleybus"]},{"b":"🚐","k":["bus","drive","minibus","van","vehicle"]},{"b":"🚑","k":["ambulance","emergency","vehicle"]},{"b":"🚒","k":["engine","fire","truck"]},{"b":"🚓","k":["5–0","car","cops","patrol","police"]},{"b":"🚔","k":["car","oncoming","police"]},{"b":"🚕","k":["cab","cabbie","car","drive","taxi","vehicle","yellow"]},{"b":"🚖","k":["cab","cabbie","cars","drove","hail","oncoming","taxi","yellow"]},{"b":"🚗","k":["automobile","car","driving","red","vehicle"]},{"b":"🚘","k":["automobile","car","cars","drove","oncoming","vehicle"]},{"b":"🚙","k":["blue","car","drive","recreational","sport","sportutility","utility","vehicle"]},{"b":"🛻","k":["automobile","car","flatbed","pick-up","pickup","transportation","truck"]},{"b":"🚚","k":["car","delivery","drive","truck","vehicle"]},{"b":"🚛","k":["articulated","car","drive","lorry","move","semi","truck","vehicle"]},{"b":"🚜","k":["tractor","vehicle"]},{"b":"🏎️","k":["car","racing"]},{"b":"🏍️","k":["motorcycle"]},{"b":"🛵","k":["motor","scooter"]},{"b":"🦽","k":["accessibility","manual","wheelchair"]},{"b":"🦼","k":["accessibility","motorized","wheelchair"]},{"b":"🛺","k":["auto","rickshaw","tuk"]},{"b":"🚲","k":["bicycle","bike","class","cycle","cycling","cyclist","gang","ride","spin","spinning"]},{"b":"🛴","k":["kick","scooter"]},{"b":"🛹","k":["board","skate","skateboard","skater","wheels"]},{"b":"🛼","k":["blades","roller","skate","skates","sport"]},{"b":"🚏","k":["bus","busstop","stop"]},{"b":"🛣️","k":["motorway"]},{"b":"🛤️","k":["railway","track"]},{"b":"🛢️","k":["drum","oil"]},{"b":"⛽","k":["diesel","fuel","fuelpump","gas","gasoline","pump","station"]},{"b":"🛞","k":["car","circle","tire","turn","vehicle","wheel"]},{"b":"🚨","k":["911","alarm","alert","beacon","car","emergency","light","police","revolving","rotating","siren"]},{"b":"🚥","k":["horizontal","intersection","light","signal","stop","stoplight","traffic"]},{"b":"🚦","k":["drove","intersection","light","semaphore","signal","stop","stoplight","traffic","vertical"]},{"b":"🛑","k":["octagonal","sign","stop"]},{"b":"🚧","k":["barrier","construction","wip"]},{"b":"⚓","k":["anchor","ship","tool"]},{"b":"🛟","k":["buoy","float","life","lifesaver","preserver","rescue","ring","safety","save","saver","swim"]},{"b":"⛵","k":["boat","resort","sailboat","sailing","sea","yacht"]},{"b":"🛶","k":["boat","canoe"]},{"b":"🚤","k":["billionaire","boat","lake","luxury","millionaire","ship","speedboat","summer","travel"]},{"b":"🛳️","k":["cruise","passenger","ship"]},{"b":"⛴️","k":["ferry"]},{"b":"🛥️","k":["boat","motor"]},{"b":"🚢","k":["boat","passenger","ship","travel"]},{"b":"✈️","k":["airplane","flight"]},{"b":"🛩️","k":["airplane","flight","small"]},{"b":"🛫","k":["aeroplane","airplane","check-in","departure","departures","flight","plane"]},{"b":"🛬","k":["aeroplane","airplane","arrival","arrivals","arriving","flight","landing","plane"]},{"b":"🪂","k":["hang-glide","parachute","parasail","skydive"]},{"b":"💺","k":["chair","seat"]},{"b":"🚁","k":["copter","helicopter","roflcopter","travel","vehicle"]},{"b":"🚟","k":["railway","suspension"]},{"b":"🚠","k":["cable","cableway","gondola","lift","mountain","ski"]},{"b":"🚡","k":["aerial","cable","car","gondola","ropeway","tramway"]},{"b":"🛰️","k":["artificial","orbit","satellite","space"]},{"b":"🚀","k":["launch","rocket","rockets","ship","space","travel"]},{"b":"🛸","k":["aliens","extra","flying","saucer","terrestrial","ufo"]},{"b":"🛎️","k":["bell","bellhop"]},{"b":"🧳","k":["bag","luggage","packing","roller","suitcase","travel"]},{"b":"⌛","k":["done","hourglass","sand","time","timer"]},{"b":"⏳","k":["done","flowing","hourglass","hours","not","sand","time","timer","waiting","yolo"]},{"b":"⌚","k":["clock","time","watch"]},{"b":"⏰","k":["alarm","clock","hours","hrs","late","morning","time","waiting"]},{"b":"⏱️","k":["stopwatch"]},{"b":"⏲️","k":["clock","timer"]},{"b":"🕰️","k":["clock","mantelpiece"]},{"b":"🕛","k":["12","12:00","clock","clock12","o’clock","time","twelve"]},{"b":"🕧","k":["12","12:30","30","clock","clock1230","thirty","time","twelve","twelve-thirty"]},{"b":"🕐","k":["1","1:00","clock","clock1","one","o’clock","time"]},{"b":"🕜","k":["1","1:30","30","clock","clock130","one","one-thirty","thirty","time"]},{"b":"🕑","k":["2","2:00","clock","clock2","o’clock","time","two"]},{"b":"🕝","k":["2","2:30","30","clock","clock230","thirty","time","two","two-thirty"]},{"b":"🕒","k":["3","3:00","clock","clock3","o’clock","three","time"]},{"b":"🕞","k":["3","30","3:30","clock","clock330","thirty","three","three-thirty","time"]},{"b":"🕓","k":["4","4:00","clock","clock4","four","o’clock","time"]},{"b":"🕟","k":["30","4","4:30","clock","clock430","four","four-thirty","thirty","time"]},{"b":"🕔","k":["5","5:00","clock","clock5","five","o’clock","time"]},{"b":"🕠","k":["30","5","5:30","clock","clock530","five","five-thirty","thirty","time"]},{"b":"🕕","k":["6","6:00","clock","clock6","o’clock","six","time"]},{"b":"🕡","k":["30","6","6:30","clock","clock630","six","six-thirty","thirty"]},{"b":"🕖","k":["0","7","7:00","clock","clock7","o’clock","seven"]},{"b":"🕢","k":["30","7","7:30","clock","clock730","seven","seven-thirty","thirty"]},{"b":"🕗","k":["8","8:00","clock","clock8","eight","o’clock","time"]},{"b":"🕣","k":["30","8","8:30","clock","clock830","eight","eight-thirty","thirty","time"]},{"b":"🕘","k":["9","9:00","clock","clock9","nine","o’clock","time"]},{"b":"🕤","k":["30","9","9:30","clock","clock930","nine","nine-thirty","thirty","time"]},{"b":"🕙","k":["0","10","10:00","clock","clock10","o’clock","ten"]},{"b":"🕥","k":["10","10:30","30","clock","clock1030","ten","ten-thirty","thirty","time"]},{"b":"🕚","k":["11","11:00","clock","clock11","eleven","o’clock","time"]},{"b":"🕦","k":["11","11:30","30","clock","clock1130","eleven","eleven-thirty","thirty","time"]},{"b":"🌑","k":["dark","moon","new","space"]},{"b":"🌒","k":["crescent","dreams","moon","space","waxing"]},{"b":"🌓","k":["first","moon","quarter","space"]},{"b":"🌔","k":["gibbous","moon","space","waxing"]},{"b":"🌕","k":["full","moon","space"]},{"b":"🌖","k":["gibbous","moon","space","waning"]},{"b":"🌗","k":["last","moon","quarter","space"]},{"b":"🌘","k":["crescent","moon","space","waning"]},{"b":"🌙","k":["crescent","moon","night","ramadan","space"]},{"b":"🌚","k":["face","moon","new","space","with"]},{"b":"🌛","k":["face","first","moon","quarter","space","with"]},{"b":"🌜","k":["dreams","face","last","moon","quarter","with"]},{"b":"🌡️","k":["thermometer"]},{"b":"☀️","k":["sun","sunny","weather"]},{"b":"🌝","k":["bright","face","full","moon","with"]},{"b":"🌞","k":["beach","bright","day","face","heat","shine","summer","sun","sunny","sunshine","weather","with"]},{"b":"🪐","k":["planet","ringed","saturn","saturnine"]},{"b":"⭐","k":["astronomy","medium","star","stars","white"]},{"b":"🌟","k":["glittery","glow","glowing","night","shining","sparkle","star","star2","win"]},{"b":"🌠","k":["falling","night","shooting","space","star","stars"]},{"b":"🌌","k":["milky","space","way"]},{"b":"☁️","k":["cloud"]},{"b":"⛅","k":["behind","cloud","cloudy","partly","sun","sunny","weather"]},{"b":"⛈️","k":["and","cloud","lightning","rain","with"]},{"b":"🌤️","k":["behind","cloud","small","sun"]},{"b":"🌥️","k":["behind","cloud","large","sun"]},{"b":"🌦️","k":["behind","cloud","rain","sun"]},{"b":"🌧️","k":["cloud","rain","with"]},{"b":"🌨️","k":["cloud","snow","with"]},{"b":"🌩️","k":["cloud","lightning","with"]},{"b":"🌪️","k":["tornado"]},{"b":"🌫️","k":["fog"]},{"b":"🌬️","k":["face","wind"]},{"b":"🌀","k":["cyclone","dizzy","hurricane","swirl","twister","typhoon","weather"]},{"b":"🌈","k":["gay","genderqueer","glbt","glbtq","lesbian","lgbt","lgbtq","lgbtqia","nature","pride","queer","rain","rainbow","trans","transgender","weather"]},{"b":"🌂","k":["closed","clothing","rain","umbrella","weather"]},{"b":"☂️","k":["open","umbrella"]},{"b":"☔","k":["clothing","drop","drops","rain","umbrella","weather","with"]},{"b":"⛱️","k":["beach_umbrella","ground","on","parasol","umbrella"]},{"b":"⚡","k":["danger","electric","electricity","high","lightning","nature","thunder","thunderbolt","voltage","zap"]},{"b":"❄️","k":["cold","snowflake","weather","winter"]},{"b":"☃️","k":["christmas","snow","snowman","winter","with"]},{"b":"⛄","k":["cold","man","snow","snowman","winter","without"]},{"b":"☄️","k":["comet"]},{"b":"🔥","k":["af","burn","fire","flame","hot","lit","litaf","tool"]},{"b":"💧","k":["cold","comic","drop","droplet","nature","sad","sweat","tear","water","weather"]},{"b":"🌊","k":["nature","ocean","sea","surf","surfer","surfing","water","wave"]}]},{"name":"Activities","icon":"⚽","emojis":[{"b":"🎃","k":["celebration","halloween","jack","jack-o-lantern","lantern","o","pumpkin"]},{"b":"🎄","k":["celebration","christmas","tree"]},{"b":"🎆","k":["boom","celebration","entertainment","festival","fireworks","yolo"]},{"b":"🎇","k":["boom","celebration","fireworks","sparkle","sparkler"]},{"b":"🧨","k":["dynamite","explosive","fire","firecracker","fireworks","light","pop","popping","spark"]},{"b":"✨","k":["*","magic","shiny","sparkle","sparkles","star"]},{"b":"🎈","k":["balloon","birthday","celebrate","celebration","party"]},{"b":"🎉","k":["awesome","birthday","celebrate","celebration","excited","hooray","party","popper","tada","woohoo"]},{"b":"🎊","k":["ball","celebrate","celebration","confetti","party","woohoo"]},{"b":"🎋","k":["banner","celebration","japanese","tanabata","tree"]},{"b":"🎍","k":["bamboo","celebration","decoration","japanese","pine","plant"]},{"b":"🎎","k":["celebration","doll","dolls","festival","japanese"]},{"b":"🎏","k":["carp","celebration","flags","streamer"]},{"b":"🎐","k":["bell","celebration","chime","wind"]},{"b":"🎑","k":["celebration","ceremony","moon","rice","scene","viewing"]},{"b":"🧧","k":["envelope","gift","good","hóngbāo","lai","luck","money","red","see"]},{"b":"🎀","k":["celebration","ribbon"]},{"b":"🎁","k":["birthday","bow","box","celebration","christmas","gift","present","surprise","wrapped"]},{"b":"🎗️","k":["reminder","ribbon"]},{"b":"🎟️","k":["admission","tickets"]},{"b":"🎫","k":["admission","stub","ticket"]},{"b":"🎖️","k":["medal","military"]},{"b":"🏆","k":["award","champion","champs","contest","prize","slay","sport","trophy","victory","win","winner","winning"]},{"b":"🏅","k":["award","gold","medal","sports","winner"]},{"b":"🥇","k":["1st","first","gold","medal","place"]},{"b":"🥈","k":["2nd","medal","place","second","silver"]},{"b":"🥉","k":["3rd","bronze","medal","place","third"]},{"b":"⚽","k":["ball","football","futbol","soccer","sport","sports"]},{"b":"⚾","k":["ball","baseball","sport","sports"]},{"b":"🥎","k":["ball","glove","softball","sports","underarm"]},{"b":"🏀","k":["ball","basketball","hoop","sport","sports"]},{"b":"🏐","k":["ball","game","volleyball"]},{"b":"🏈","k":["american","ball","bowl","football","sport","sports","super"]},{"b":"🏉","k":["ball","football","rugby","sport"]},{"b":"🎾","k":["ball","racquet","sport","sports","tennis"]},{"b":"🥏","k":["disc","flying","ultimate"]},{"b":"🎳","k":["ball","bowling","game","sport","strike"]},{"b":"🏏","k":["ball","bat","cricket","game"]},{"b":"🏑","k":["ball","field","game","hockey","stick"]},{"b":"🏒","k":["game","hockey","ice","puck","stick"]},{"b":"🥍","k":["ball","goal","lacrosse","sports","stick"]},{"b":"🏓","k":["ball","bat","game","paddle","ping","pingpong","pong","table","tennis"]},{"b":"🏸","k":["badminton","birdie","game","racquet","shuttlecock"]},{"b":"🥊","k":["boxing","glove"]},{"b":"🥋","k":["arts","judo","karate","martial","taekwondo","uniform"]},{"b":"🥅","k":["goal","net"]},{"b":"⛳","k":["flag","golf","hole","sport"]},{"b":"⛸️","k":["ice","skate","skating"]},{"b":"🎣","k":["and","entertainment","fish","fishing","pole","sport"]},{"b":"🤿","k":["diving","mask","scuba","snorkeling"]},{"b":"🎽","k":["athletics","marathon","running","sash","shirt","with"]},{"b":"🎿","k":["ski","skis","snow","sport"]},{"b":"🛷","k":["luge","sled","sledge","sleigh","snow","toboggan"]},{"b":"🥌","k":["curling","game","rock","stone"]},{"b":"🎯","k":["bull","bullseye","dart","direct","entertainment","game","hit","target"]},{"b":"🪀","k":["fluctuate","toy","yo","yo-yo"]},{"b":"🪁","k":["fly","kite","soar"]},{"b":"🔫","k":["gun","handgun","pistol","revolver","shoot","tool","water","weapon"]},{"b":"🎱","k":["8","8ball","ball","billiard","billiards","eight","game","pool"]},{"b":"🔮","k":["ball","crystal","fairy","fairytale","fantasy","fortune","future","magic","tale","tool"]},{"b":"🪄","k":["magic","magician","wand","witch","wizard"]},{"b":"🎮","k":["console","controller","entertainment","game","play","video"]},{"b":"🕹️","k":["joystick"]},{"b":"🎰","k":["casino","gamble","gambling","game","machine","slot","slots"]},{"b":"🎲","k":["dice","die","entertainment","gambling","game"]},{"b":"🧩","k":["clue","interlocking","jigsaw","piece","puzzle"]},{"b":"🧸","k":["bear","plaything","plush","stuffed","teddy","toy"]},{"b":"🪅","k":["candy","celebrate","celebration","cinco","de","festive","mayo","party","pinada","pinata","piñata"]},{"b":"🪩","k":["ball","dance","disco","glitter","mirror","party"]},{"b":"🪆","k":["babooshka","baboushka","babushka","doll","dolls","matryoshka","nesting","russia"]},{"b":"♠️","k":["spade","spades","suit"]},{"b":"♥️","k":["heart","hearts","suit"]},{"b":"♦️","k":["diamond","diamonds","suit"]},{"b":"♣️","k":["club","clubs","suit"]},{"b":"♟️","k":["chess","pawn"]},{"b":"🃏","k":["black","card","game","joker","wildcard"]},{"b":"🀄","k":["dragon","game","mahjong","red"]},{"b":"🎴","k":["card","cards","flower","game","japanese","playing"]},{"b":"🎭","k":["actor","actress","art","arts","drama","entertainment","mask","performing","theater","theatre","thespian"]},{"b":"🖼️","k":["framed","picture"]},{"b":"🎨","k":["art","artist","artsy","arty","colorful","creative","design","entertainment","museum","paint","painter","painting","palette"]},{"b":"🧵","k":["needle","sewing","spool","string","thread"]},{"b":"🪡","k":["embroidery","needle","sew","sewing","stitches","sutures","tailoring","thread"]},{"b":"🧶","k":["ball","crochet","knit","yarn"]},{"b":"🪢","k":["cord","knot","rope","tangled","tie","twine","twist"]}]},{"name":"Objects","icon":"💡","emojis":[{"b":"👓","k":["clothing","eye","eyeglasses","eyewear","glasses"]},{"b":"🕶️","k":["dark","sunglasses"]},{"b":"🥽","k":["dive","eye","goggles","protection","scuba","swimming","welding"]},{"b":"🥼","k":["clothes","coat","doctor","dr","experiment","jacket","lab","scientist","white"]},{"b":"🦺","k":["emergency","safety","vest"]},{"b":"👔","k":["clothing","employed","formal","necktie","serious","shirt","tie"]},{"b":"👕","k":["blue","casual","clothes","clothing","collar","dressed","shirt","shopping","t-shirt","tshirt","weekend"]},{"b":"👖","k":["blue","casual","clothes","clothing","denim","dressed","jeans","pants","shopping","trousers","weekend"]},{"b":"🧣","k":["bundle","cold","neck","scarf","up"]},{"b":"🧤","k":["gloves","hand"]},{"b":"🧥","k":["brr","bundle","coat","cold","jacket","up"]},{"b":"🧦","k":["socks","stocking"]},{"b":"👗","k":["clothes","clothing","dress","dressed","fancy","shopping"]},{"b":"👘","k":["clothing","comfortable","kimono"]},{"b":"🥻","k":["clothing","dress","sari"]},{"b":"🩱","k":["bathing","one","one-piece","piece","suit","swimsuit"]},{"b":"🩲","k":["bathing","brief","briefs","one-piece","suit","swim","swimsuit","underwear"]},{"b":"🩳","k":["bathing","pants","shorts","suit","swimsuit","underwear"]},{"b":"👙","k":["bathing","beach","bikini","clothing","pool","suit","swim"]},{"b":"👚","k":["blouse","clothes","clothing","collar","dress","dressed","lady","shirt","shopping","woman","womans","woman’s"]},{"b":"🪭","k":["clack","clap","cool","cooling","dance","fan","flirt","flutter","folding","hand","hot","sensu","shy"]},{"b":"👛","k":["clothes","clothing","coin","dress","fancy","handbag","purse","shopping"]},{"b":"👜","k":["bag","clothes","clothing","dress","handbag","lady","purse","shopping"]},{"b":"👝","k":["bag","clothes","clothing","clutch","dress","handbag","pouch","purse"]},{"b":"🛍️","k":["bags","shopping"]},{"b":"🎒","k":["backpack","backpacking","bag","bookbag","education","rucksack","satchel","school"]},{"b":"🩴","k":["beach","flip","flop","sandal","sandals","shoe","thong","thongs","zōri"]},{"b":"👞","k":["brown","clothes","clothing","feet","foot","kick","man","mans","man’s","shoe","shoes","shopping"]},{"b":"👟","k":["athletic","clothes","clothing","fast","kick","running","shoe","shoes","shopping","sneaker","sport","tennis"]},{"b":"🥾","k":["backpacking","boot","brown","camping","hiking","outdoors","shoe"]},{"b":"🥿","k":["ballet","comfy","flat","flats","shoe","slip-on","slipper"]},{"b":"👠","k":["clothes","clothing","dress","fashion","heel","heels","high","high-heeled","shoe","shoes","shopping","stiletto","woman"]},{"b":"👡","k":["clothing","sandal","shoe","woman","woman’s"]},{"b":"🩰","k":["ballet","dance","shoes"]},{"b":"👢","k":["boot","clothes","clothing","dress","shoe","shoes","shopping","woman","woman’s"]},{"b":"🪮","k":["afro","comb","groom","hair","pick"]},{"b":"👑","k":["clothing","crown","family","king","medieval","queen","royal","royalty","win"]},{"b":"👒","k":["clothes","clothing","garden","hat","hats","party","woman","womans","woman’s"]},{"b":"🎩","k":["classy","clothes","clothing","fancy","formal","hat","magic","top","tophat"]},{"b":"🎓","k":["board","cap","celebration","clothing","college","education","graduation","hat","mortar","scholar","university"]},{"b":"🧢","k":["baseball","bent","billed","cap","dad","hat"]},{"b":"🪖","k":["army","helmet","military","soldier","war","warrior"]},{"b":"⛑️","k":["helmet","rescue","worker","worker’s"]},{"b":"📿","k":["beads","clothing","necklace","prayer","religion"]},{"b":"💄","k":["cosmetics","date","lipstick","makeup"]},{"b":"💍","k":["diamond","engaged","engagement","marriage","married","ring","romance","shiny","sparkling","wedding"]},{"b":"💎","k":["diamond","engagement","gem","jewel","money","romance","stone","wedding"]},{"b":"🔇","k":["mute","muted","quiet","silent","sound","speaker","volume"]},{"b":"🔈","k":["low","soft","sound","speaker","volume"]},{"b":"🔉","k":["medium","sound","speaker","volume"]},{"b":"🔊","k":["high","loud","music","sound","speaker","volume"]},{"b":"📢","k":["address","announcement","communication","loud","loudspeaker","public","sound"]},{"b":"📣","k":["cheering","mega","megaphone","sound"]},{"b":"📯","k":["horn","post","postal"]},{"b":"🔔","k":["bell","break","church","notification","sound"]},{"b":"🔕","k":["bell","forbidden","mute","no","not","off","prohibited","quiet","silent","slash","sound","volume","with"]},{"b":"🎼","k":["music","musical","note","score"]},{"b":"🎵","k":["music","musical","note","sound"]},{"b":"🎶","k":["music","musical","note","notes","sound"]},{"b":"🎙️","k":["microphone","podcast","studio"]},{"b":"🎚️","k":["level","slider"]},{"b":"🎛️","k":["control","knobs"]},{"b":"🎤","k":["karaoke","mic","microphone","music","sing","sound"]},{"b":"🎧","k":["earbud","earphones","headphone","headphones","music","sound"]},{"b":"📻","k":["entertainment","podcast","radio","tbt","video"]},{"b":"🎷","k":["instrument","music","sax","saxophone"]},{"b":"🪗","k":["accordion","box","concertina","instrument","music","squeeze","squeezebox"]},{"b":"🎸","k":["guitar","instrument","music","rock","strat"]},{"b":"🎹","k":["instrument","keyboard","music","musical","piano"]},{"b":"🎺","k":["instrument","music","trumpet"]},{"b":"🎻","k":["instrument","music","violin"]},{"b":"🪕","k":["banjo","music","stringed"]},{"b":"🥁","k":["drum","drumsticks","music"]},{"b":"🪘","k":["beat","conga","drum","instrument","long","rhythm"]},{"b":"🪇","k":["cha","dance","instrument","maracas","music","party","percussion","rattle","shake","shaker"]},{"b":"🪈","k":["band","fife","flautist","flute","instrument","marching","music","orchestra","piccolo","pipe","recorder","woodwind"]},{"b":"📱","k":["cell","communication","iphone","mobile","phone","smartphone","telephone"]},{"b":"📲","k":["arrow","build","call","calling","cell","communication","incoming","mobile","phone","receive","telephone","with"]},{"b":"☎️","k":["phone","telephone"]},{"b":"📞","k":["call","communication","phone","receiver","telephone","voip"]},{"b":"📟","k":["communication","pager"]},{"b":"📠","k":["communication","fax","machine"]},{"b":"🔋","k":["battery","power"]},{"b":"🪫","k":["battery","drained","electronic","energy","low","power"]},{"b":"🔌","k":["electric","electricity","plug"]},{"b":"💻","k":["computer","desktop","laptop","office","pc","personal","screen"]},{"b":"🖥️","k":["computer","desktop"]},{"b":"🖨️","k":["printer"]},{"b":"⌨️","k":["keyboard"]},{"b":"🖱️","k":["computer","mouse"]},{"b":"🖲️","k":["trackball"]},{"b":"💽","k":["computer","disk","minidisc","minidisk","optical"]},{"b":"💾","k":["computer","disk","floppy","save"]},{"b":"💿","k":["blu-ray","cd","computer","disk","dvd","optical"]},{"b":"📀","k":["blu-ray","cd","computer","disk","dvd","optical"]},{"b":"🧮","k":["abacus","calculation","calculator"]},{"b":"🎥","k":["bollywood","camera","cinema","film","hollywood","movie","record","video"]},{"b":"🎞️","k":["film","frames","strip"]},{"b":"📽️","k":["film","projector"]},{"b":"🎬","k":["action","board","clapper","film","movie"]},{"b":"📺","k":["television","tv","video"]},{"b":"📷","k":["camera","photo","selfie","snap","tbt","trip","video"]},{"b":"📸","k":["camera","flash","photo","video","with"]},{"b":"📹","k":["camcorder","camera","tbt","video"]},{"b":"📼","k":["old","school","tape","vcr","vhs","video","videocassette"]},{"b":"🔍","k":["glass","lab","left","left-pointing","mag","magnifying","science","search","tilted","tool","zoom"]},{"b":"🔎","k":["contact","glass","lab","mag","magnifying","right","right-pointing","science","search","tilted","tool"]},{"b":"🕯️","k":["candle"]},{"b":"💡","k":["bulb","comic","electric","idea","light"]},{"b":"🔦","k":["electric","flashlight","light","tool","torch"]},{"b":"🏮","k":["bar","izakaya","lantern","light","paper","red","restaurant"]},{"b":"🪔","k":["diya","lamp","light","oil"]},{"b":"📔","k":["book","cover","decorated","decorative","education","notebook","school","with","writing"]},{"b":"📕","k":["book","closed","education"]},{"b":"📖","k":["book","education","fantasy","knowledge","library","novels","open","reading"]},{"b":"📗","k":["book","education","fantasy","green","library","reading"]},{"b":"📘","k":["blue","book","education","fantasy","library","reading"]},{"b":"📙","k":["book","education","fantasy","library","orange","reading"]},{"b":"📚","k":["book","books","education","fantasy","knowledge","library","novels","reading","school","study"]},{"b":"📓","k":["notebook"]},{"b":"📒","k":["ledger","notebook"]},{"b":"📃","k":["curl","document","page","paper","with"]},{"b":"📜","k":["document","paper","scroll"]},{"b":"📄","k":["document","facing","page","paper","up"]},{"b":"📰","k":["communication","news","newspaper","paper","press"]},{"b":"🗞️","k":["newspaper","press","roll","rolled-up"]},{"b":"📑","k":["bookmark","mark","marker","tabs"]},{"b":"🔖","k":["bookmark","mark"]},{"b":"🏷️","k":["label","tag"]},{"b":"💰","k":["bag","bank","bet","billion","cash","cost","cream","dollar","gold","million","money","moneybag","paid","paying","pot","rich","win"]},{"b":"🪙","k":["coin","dollar","euro","gold","metal","money","rich","silver","treasure"]},{"b":"💴","k":["bank","banknote","bill","currency","money","note","yen"]},{"b":"💵","k":["bank","banknote","bill","currency","dollar","money","note"]},{"b":"💶","k":["100","bank","banknote","bill","currency","euro","money","note","rich"]},{"b":"💷","k":["bank","banknote","bill","billion","cash","currency","money","note","pound","pounds"]},{"b":"💸","k":["bank","banknote","bill","billion","cash","dollar","fly","million","money","note","pay","wings","with"]},{"b":"💳","k":["bank","card","cash","charge","credit","money","pay","subscription"]},{"b":"🧾","k":["accounting","bookkeeping","evidence","invoice","proof","receipt"]},{"b":"💹","k":["bank","chart","currency","graph","growth","increasing","market","money","rise","trend","upward","with","yen"]},{"b":"✉️","k":["email","envelope","letter"]},{"b":"📧","k":["e-mail","email","letter","mail"]},{"b":"📨","k":["delivering","e-mail","email","envelope","incoming","letter","mail","receive","sent"]},{"b":"📩","k":["arrow","communication","down","e-mail","email","envelope","letter","mail","outgoing","send","sent","with"]},{"b":"📤","k":["box","email","letter","mail","outbox","sent","tray"]},{"b":"📥","k":["box","email","inbox","letter","mail","receive","tray","zero"]},{"b":"📦","k":["box","communication","delivery","package","parcel","shipping"]},{"b":"📫","k":["closed","communication","flag","mail","mailbox","postbox","raised","with"]},{"b":"📪","k":["closed","flag","lowered","mail","mailbox","postbox","with"]},{"b":"📬","k":["flag","mail","mailbox","open","postbox","raised","with"]},{"b":"📭","k":["flag","lowered","mail","mailbox","no","open","postbox","with"]},{"b":"📮","k":["mail","mailbox","postbox"]},{"b":"🗳️","k":["ballot","box","with"]},{"b":"✏️","k":["pencil","pencil2"]},{"b":"✒️","k":["black","nib"]},{"b":"🖋️","k":["fountain","pen"]},{"b":"🖊️","k":["pen"]},{"b":"🖌️","k":["paintbrush"]},{"b":"🖍️","k":["crayon"]},{"b":"📝","k":["communication","document","media","memo","note","notes","pencil"]},{"b":"💼","k":["briefcase","business","office"]},{"b":"📁","k":["directory","file","folder"]},{"b":"📂","k":["file","folder","open"]},{"b":"🗂️","k":["card","dividers","index"]},{"b":"📅","k":["calendar","date","schedule"]},{"b":"📆","k":["calendar","schedule","tear-off"]},{"b":"🗒️","k":["notepad","spiral"]},{"b":"🗓️","k":["calendar","spiral"]},{"b":"📇","k":["card","index","old","rolodex","school"]},{"b":"📈","k":["chart","data","graph","growth","increasing","metrics","right","trend","up","upward","upwards","with"]},{"b":"📉","k":["chart","data","decreasing","down","downward","downwards","graph","metrics","negative","trend","with"]},{"b":"📊","k":["bar","chart","data","graph","metrics","stats"]},{"b":"📋","k":["clipboard","do","list","notes"]},{"b":"📌","k":["collage","location","pin","pushpin"]},{"b":"📍","k":["location","map","pin","pushpin","round"]},{"b":"📎","k":["paperclip"]},{"b":"🖇️","k":["linked","paperclips"]},{"b":"📏","k":["angle","edge","math","ruler","straight","straightedge"]},{"b":"📐","k":["angle","math","rule","ruler","set","slide","triangle","triangular"]},{"b":"✂️","k":["cut","scissors"]},{"b":"🗃️","k":["box","card","file"]},{"b":"🗄️","k":["cabinet","file"]},{"b":"🗑️","k":["trash","wastebasket"]},{"b":"🔒","k":["closed","lock","locked","private","security"]},{"b":"🔓","k":["cracked","lock","open","security","unlock","unlocked"]},{"b":"🔏","k":["ink","lock","locked","nib","pen","privacy","with"]},{"b":"🔐","k":["bike","closed","key","lock","locked","secure","security","with"]},{"b":"🔑","k":["key","keys","lock","major","password","unlock"]},{"b":"🗝️","k":["key","old"]},{"b":"🔨","k":["hammer","home","improvement","repairs","tool"]},{"b":"🪓","k":["ax","axe","chop","hatchet","split","wood"]},{"b":"⛏️","k":["pick"]},{"b":"⚒️","k":["and","hammer","pick"]},{"b":"🛠️","k":["and","hammer","wrench"]},{"b":"🗡️","k":["dagger"]},{"b":"⚔️","k":["crossed","swords"]},{"b":"💣","k":["bomb","boom","comic","dangerous","explosion","hot"]},{"b":"🪃","k":["boomerang","rebound","repercussion","weapon"]},{"b":"🏹","k":["and","archer","archery","arrow","bow","sagittarius","tool","weapon","zodiac"]},{"b":"🛡️","k":["shield"]},{"b":"🪚","k":["carpenter","carpentry","cut","lumber","saw","tool","trim"]},{"b":"🔧","k":["home","improvement","spanner","tool","wrench"]},{"b":"🪛","k":["flathead","handy","screw","screwdriver","tool"]},{"b":"🔩","k":["and","bolt","home","improvement","nut","tool"]},{"b":"⚙️","k":["gear"]},{"b":"🗜️","k":["clamp"]},{"b":"⚖️","k":["balance","scale"]},{"b":"🦯","k":["accessibility","blind","cane","probing","white"]},{"b":"🔗","k":["link","links"]},{"b":"⛓️","k":["chains"]},{"b":"🪝","k":["catch","crook","curve","ensnare","hook","point","selling"]},{"b":"🧰","k":["box","chest","mechanic","red","tool","toolbox"]},{"b":"🧲","k":["attraction","horseshoe","magnet","magnetic","negative","positive","shape","u"]},{"b":"🪜","k":["climb","ladder","rung","step"]},{"b":"⚗️","k":["alembic"]},{"b":"🧪","k":["chemist","chemistry","experiment","lab","science","test","tube"]},{"b":"🧫","k":["bacteria","biologist","biology","culture","dish","lab","petri"]},{"b":"🧬","k":["biologist","dna","evolution","gene","genetics","life"]},{"b":"🔬","k":["experiment","investigate","lab","laboratory","microscope","science","tool"]},{"b":"🔭","k":["contact","extraterrestrial","science","telescope","tool"]},{"b":"📡","k":["aliens","antenna","contact","dish","satellite","science","signal"]},{"b":"💉","k":["doctor","flu","health","hospital","medicine","needle","shot","sick","syringe","tool","vaccination"]},{"b":"🩸","k":["bleed","blood","donation","drop","injury","medicine","menstruation","of"]},{"b":"💊","k":["doctor","drugs","health","medicated","medicine","pill","pills","sick","vitamin"]},{"b":"🩹","k":["adhesive","bandage"]},{"b":"🩼","k":["aid","cane","crutch","disability","help","hurt","injured","mobility","stick"]},{"b":"🩺","k":["doctor","heart","medicine","stethoscope"]},{"b":"🩻","k":["bones","doctor","medical","ray","skeleton","skull","x","x-ray","xray"]},{"b":"🚪","k":["back","closet","door","front"]},{"b":"🛗","k":["accessibility","elevator","hoist","lift"]},{"b":"🪞","k":["makeup","mirror","reflection","reflector","speculum"]},{"b":"🪟","k":["air","frame","fresh","opening","transparent","view","window"]},{"b":"🛏️","k":["bed"]},{"b":"🛋️","k":["and","couch","lamp"]},{"b":"🪑","k":["chair","seat","sit"]},{"b":"🚽","k":["bathroom","toilet","wc"]},{"b":"🪠","k":["cup","force","plumber","plunger","poop","suction","toilet"]},{"b":"🚿","k":["bath","shower","water"]},{"b":"🛁","k":["bath","bathtub"]},{"b":"🪤","k":["bait","cheese","lure","mouse","mousetrap","snare","trap"]},{"b":"🪒","k":["razor","sharp","shave"]},{"b":"🧴","k":["bottle","lotion","moisturizer","shampoo","sunscreen"]},{"b":"🧷","k":["diaper","pin","punk","rock","safety"]},{"b":"🧹","k":["broom","cleaning","sweeping","witch"]},{"b":"🧺","k":["basket","farming","laundry","picnic"]},{"b":"🧻","k":["of","paper","roll","toilet","towels"]},{"b":"🪣","k":["bucket","cask","pail","vat"]},{"b":"🧼","k":["bar","bathing","clean","cleaning","lather","soap","soapdish"]},{"b":"🫧","k":["bubble","bubbles","burp","clean","floating","pearl","soap","underwater"]},{"b":"🪥","k":["bathroom","brush","clean","dental","hygiene","teeth","toiletry","toothbrush"]},{"b":"🧽","k":["absorbing","cleaning","porous","soak","sponge"]},{"b":"🧯","k":["extinguish","extinguisher","fire","quench"]},{"b":"🛒","k":["cart","shopping","trolley"]},{"b":"🚬","k":["cigarette","smoking"]},{"b":"⚰️","k":["coffin","funeral"]},{"b":"🪦","k":["cemetery","dead","grave","graveyard","headstone","memorial","rip","tomb","tombstone"]},{"b":"⚱️","k":["funeral","urn"]},{"b":"🧿","k":["amulet","bead","blue","charm","evil-eye","nazar","talisman"]},{"b":"🪬","k":["amulet","fatima","fortune","guide","hamsa","hand","mary","miriam","palm","protect","protection"]},{"b":"🗿","k":["face","moai","moyai","statue","stone","stoneface","travel"]},{"b":"🪧","k":["card","demonstration","notice","picket","placard","plaque","protest","sign"]},{"b":"🪪","k":["card","credentials","document","id","identification","license","security"]}]},{"name":"Symbols","icon":"❤️","emojis":[{"b":"🏧","k":["atm","automated","bank","cash","money","sign","teller"]},{"b":"🚮","k":["bin","in","its","litter","litterbin","place","put","sign"]},{"b":"🚰","k":["drinking","potable","water"]},{"b":"♿","k":["access","accessibility","handicap","symbol","wheelchair"]},{"b":"🚹","k":["bathroom","lavatory","man","mens","men’s","restroom","room","toilet","wc"]},{"b":"🚺","k":["bathroom","lavatory","restroom","room","toilet","wc","woman","womens","women’s"]},{"b":"🚻","k":["bathroom","lavatory","restroom","toilet","wc"]},{"b":"🚼","k":["baby","changing","symbol"]},{"b":"🚾","k":["bathroom","closet","lavatory","restroom","toilet","water","wc"]},{"b":"🛂","k":["control","passport"]},{"b":"🛃","k":["customs","packing"]},{"b":"🛄","k":["airport","arrived","baggage","bags","case","checked","claim","journey","packing","plane","ready","travel","trip"]},{"b":"🛅","k":["baggage","case","left","locker","luggage"]},{"b":"⚠️","k":["warning","wip"]},{"b":"🚸","k":["child","children","crossing","pedestrian","traffic"]},{"b":"⛔","k":["do","entry","fail","forbidden","limit","no","not","pass","prohibited","traffic"]},{"b":"🚫","k":["block","entry","forbidden","no","not","prohibited","sign","smoke"]},{"b":"🚳","k":["bicycle","bicycles","bike","forbidden","no","not","prohibited"]},{"b":"🚭","k":["forbidden","no","not","prohibited","smoke","smoking"]},{"b":"🚯","k":["do","forbidden","litter","littering","no","not","prohibited"]},{"b":"🚱","k":["dry","non-drinking","non-potable","prohibited","water"]},{"b":"🚷","k":["forbidden","no","not","pedestrian","pedestrians","prohibited"]},{"b":"📵","k":["cell","forbidden","mobile","no","not","phone","phones","prohibited","telephone"]},{"b":"🔞","k":["18","age","eighteen","forbidden","no","not","one","prohibited","restriction","under","underage"]},{"b":"☢️","k":["radioactive"]},{"b":"☣️","k":["biohazard"]},{"b":"⬆️","k":["arrow","up"]},{"b":"↗️","k":["arrow","right","up-right","upper"]},{"b":"➡️","k":["arrow","right"]},{"b":"↘️","k":["arrow","down-right","lower","right"]},{"b":"⬇️","k":["arrow","down"]},{"b":"↙️","k":["arrow","down-left","left","lower"]},{"b":"⬅️","k":["arrow","left"]},{"b":"↖️","k":["arrow","left","up-left","upper"]},{"b":"↕️","k":["arrow","down","up","up-down"]},{"b":"↔️","k":["arrow","left","left-right","right"]},{"b":"↩️","k":["arrow","curving","hook","left","leftwards","return","right","with"]},{"b":"↪️","k":["arrow","curving","hook","left","right"]},{"b":"⤴️","k":["arrow","curving","heading","right","up"]},{"b":"⤵️","k":["arrow","curving","down","heading","right"]},{"b":"🔃","k":["arrow","arrows","clockwise","refresh","reload","vertical"]},{"b":"🔄","k":["again","anticlockwise","arrow","arrows","button","counterclockwise","deja","refresh","rewindershins","sync","vu"]},{"b":"🔙","k":["arrow","back"]},{"b":"🔚","k":["arrow","end"]},{"b":"🔛","k":["arrow","mark","on","on!"]},{"b":"🔜","k":["arrow","brb","omw","soon"]},{"b":"🔝","k":["arrow","homie","top","up"]},{"b":"🛐","k":["of","place","pray","religion","worship"]},{"b":"⚛️","k":["atom","symbol"]},{"b":"🕉️","k":["om"]},{"b":"✡️","k":["david","of","star"]},{"b":"☸️","k":["dharma","of","wheel"]},{"b":"☯️","k":["yang","yin"]},{"b":"✝️","k":["cross","latin"]},{"b":"☦️","k":["cross","orthodox"]},{"b":"☪️","k":["and","crescent","star"]},{"b":"☮️","k":["peace","symbol"]},{"b":"🕎","k":["candelabrum","candlestick","hanukkah","jewish","judaism","menorah","religion"]},{"b":"🔯","k":["dotted","fortune","jewish","judaism","pointed","six","six-pointed","star"]},{"b":"🪯","k":["deg","fateh","khalsa","khanda","religion","sikh","sikhism","tegh"]},{"b":"♈","k":["aries","horoscope","ram","zodiac"]},{"b":"♉","k":["bull","horoscope","ox","taurus","zodiac"]},{"b":"♊","k":["gemini","horoscope","twins","zodiac"]},{"b":"♋","k":["cancer","crab","horoscope","zodiac"]},{"b":"♌","k":["horoscope","leo","lion","zodiac"]},{"b":"♍","k":["horoscope","virgo","zodiac"]},{"b":"♎","k":["balance","horoscope","justice","libra","scales","zodiac"]},{"b":"♏","k":["horoscope","scorpio","scorpion","scorpius","zodiac"]},{"b":"♐","k":["archer","horoscope","sagittarius","zodiac"]},{"b":"♑","k":["capricorn","goat","horoscope","zodiac"]},{"b":"♒","k":["aquarius","bearer","horoscope","water","zodiac"]},{"b":"♓","k":["fish","horoscope","pisces","zodiac"]},{"b":"⛎","k":["bearer","ophiuchus","serpent","snake","zodiac"]},{"b":"🔀","k":["arrow","arrows","button","crossed","rightwards","shuffle","tracks","twisted"]},{"b":"🔁","k":["arrow","button","clockwise","loop","repeat"]},{"b":"🔂","k":["arrow","button","clockwise","once","one","repeat","single"]},{"b":"▶️","k":["arrow","button","forward","play"]},{"b":"⏩","k":["arrow","button","double","fast","fast-forward","forward"]},{"b":"⏭️","k":["button","next","track"]},{"b":"⏯️","k":["button","or","pause","play"]},{"b":"◀️","k":["arrow","backward","button","reverse"]},{"b":"⏪","k":["arrow","button","double","fast","reverse","rewind"]},{"b":"⏮️","k":["button","last","previous","track"]},{"b":"🔼","k":["arrow","button","red","small","up","upwards"]},{"b":"⏫","k":["arrow","button","double","fast","up"]},{"b":"🔽","k":["arrow","button","down","downwards","red","small"]},{"b":"⏬","k":["arrow","button","double","down","fast"]},{"b":"⏸️","k":["button","pause"]},{"b":"⏹️","k":["button","stop"]},{"b":"⏺️","k":["button","record"]},{"b":"⏏️","k":["button","eject"]},{"b":"🎦","k":["camera","cinema","film","movie"]},{"b":"🔅","k":["brightness","button","dim","low"]},{"b":"🔆","k":["bright","brightness","button","high","light"]},{"b":"📶","k":["antenna","bar","bars","cell","communication","mobile","phone","signal","strength","telephone","wifi"]},{"b":"🛜","k":["broadband","computer","connectivity","hotspot","internet","network","router","smartphone","wi-fi","wifi","wireless","wlan"]},{"b":"📳","k":["cell","communication","mobile","mode","phone","telephone","vibration"]},{"b":"📴","k":["cell","mobile","mute","off","phone","telephone"]},{"b":"♀️","k":["female","sign"]},{"b":"♂️","k":["male","sign"]},{"b":"⚧️","k":["symbol","transgender"]},{"b":"✖️","k":["heavy","multiplication","multiply","x"]},{"b":"➕","k":["+","heavy","plus","sign"]},{"b":"➖","k":["-","heavy","math","minus","sign","−"]},{"b":"➗","k":["divide","division","heavy","math","sign","÷"]},{"b":"🟰","k":["answer","equal","equality","equals","heavy","math","sign"]},{"b":"♾️","k":["infinity"]},{"b":"‼️","k":["bangbang","double","exclamation","mark"]},{"b":"⁉️","k":["exclamation","interrobang","mark","question"]},{"b":"❓","k":["?","confused","mark","punctuation","question","red"]},{"b":"❔","k":["?","grey","mark","outlined","punctuation","question","white"]},{"b":"❕","k":["!","exclamation","grey","mark","outlined","punctuation","white"]},{"b":"❗","k":["!","bang","exclamation","heavy","mark","punctuation","red"]},{"b":"〰️","k":["dash","wavy"]},{"b":"💱","k":["bank","currency","exchange","money"]},{"b":"💲","k":["billion","cash","charge","currency","dollar","heavy","million","money","pay","sign"]},{"b":"⚕️","k":["medical","symbol"]},{"b":"♻️","k":["environment","green","recycle","recycling","symbol"]},{"b":"⚜️","k":["de","fleur","fleur-de-lis","lis"]},{"b":"🔱","k":["anchor","emblem","poseidon","ship","tool","trident"]},{"b":"📛","k":["badge","name"]},{"b":"🔰","k":["beginner","chevron","for","green","japanese","leaf","symbol","tool","yellow"]},{"b":"⭕","k":["circle","heavy","hollow","large","o","red"]},{"b":"✅","k":["button","check","checked","checkmark","complete","completed","done","fixed","mark","tick","white","✓"]},{"b":"☑️","k":["ballot","box","check","with"]},{"b":"✔️","k":["check","heavy","mark"]},{"b":"❌","k":["cancel","cross","mark","multiplication","multiply","x","×"]},{"b":"❎","k":["button","cross","mark","multiplication","multiply","negative","square","squared","x","×"]},{"b":"➰","k":["curl","curly","loop"]},{"b":"➿","k":["curl","curly","double","loop"]},{"b":"〽️","k":["alternation","mark","part"]},{"b":"✳️","k":["asterisk","eight","eight-spoked","spoked"]},{"b":"✴️","k":["black","eight","eight-pointed","pointed","star"]},{"b":"❇️","k":["sparkle"]},{"b":"©️","k":["copyright"]},{"b":"®️","k":["registered"]},{"b":"™️","k":["mark","tm","trade","trademark"]},{"b":"#️⃣","k":["hash","keycap:","number"]},{"b":"*️⃣","k":["asterisk","keycap:"]},{"b":"0️⃣","k":["keycap:","zero"]},{"b":"1️⃣","k":["keycap:","one"]},{"b":"2️⃣","k":["keycap:","two"]},{"b":"3️⃣","k":["keycap:","three"]},{"b":"4️⃣","k":["four","keycap:"]},{"b":"5️⃣","k":["five","keycap:"]},{"b":"6️⃣","k":["keycap:","six"]},{"b":"7️⃣","k":["keycap:","seven"]},{"b":"8️⃣","k":["eight","keycap:"]},{"b":"9️⃣","k":["keycap:","nine"]},{"b":"🔟","k":["keycap","keycap:","ten"]},{"b":"🔠","k":["abcd","capital","input","latin","letters","uppercase"]},{"b":"🔡","k":["abcd","input","latin","letters","lowercase"]},{"b":"🔢","k":["1234","input","numbers"]},{"b":"🔣","k":["%","&","input","symbols","♪","〒"]},{"b":"🔤","k":["abc","alphabet","input","latin","letters"]},{"b":"🅰️","k":["(blood","a","button","type)"]},{"b":"🆎","k":["(blood","ab","blood","button","type","type)"]},{"b":"🅱️","k":["(blood","b","button","type)"]},{"b":"🆑","k":["button","cl"]},{"b":"🆒","k":["button","cool"]},{"b":"🆓","k":["button","free"]},{"b":"ℹ️","k":["information","source"]},{"b":"🆔","k":["button","id","identity"]},{"b":"Ⓜ️","k":["circled","m"]},{"b":"🆕","k":["button","fresh","new"]},{"b":"🆖","k":["button","ng"]},{"b":"🅾️","k":["(blood","button","o2","type)"]},{"b":"🆗","k":["button","ok","okay","yes"]},{"b":"🅿️","k":["button","parking"]},{"b":"🆘","k":["button","emergency","help","sos"]},{"b":"🆙","k":["button","mark","up","up!"]},{"b":"🆚","k":["button","versus","vs"]},{"b":"🈁","k":["button","here","japanese","katakana","koko","“here”"]},{"b":"🈂️","k":["button","charge”","japanese","sa","“service"]},{"b":"🈷️","k":["amount”","button","japanese","u6708","“monthly"]},{"b":"🈶","k":["button","charge","charge”","free","ideograph","japanese","not","u6709","“not"]},{"b":"🈯","k":["button","ideograph","japanese","reserved","u6307","“reserved”"]},{"b":"🉐","k":["advantage","bargain","button","ideograph","japanese","“bargain”"]},{"b":"🈹","k":["button","discount","ideograph","japanese","u5272","“discount”"]},{"b":"🈚","k":["button","charge","charge”","free","ideograph","japanese","u7121","“free"]},{"b":"🈲","k":["button","ideograph","japanese","prohibited","u7981","“prohibited”"]},{"b":"🉑","k":["accept","acceptable","button","ideograph","japanese","“acceptable”"]},{"b":"🈸","k":["application","button","ideograph","japanese","u7533","“application”"]},{"b":"🈴","k":["button","grade","grade”","ideograph","japanese","passing","u5408","“passing"]},{"b":"🈳","k":["button","ideograph","japanese","u7a7a","vacancy","“vacancy”"]},{"b":"㊗️","k":["button","congratulations","japanese","“congratulations”"]},{"b":"㊙️","k":["button","japanese","secret","“secret”"]},{"b":"🈺","k":["business","business”","button","for","ideograph","japanese","open","u55b6","“open"]},{"b":"🈵","k":["button","ideograph","japanese","no","u6e80","vacancy","vacancy”","“no"]},{"b":"🔴","k":["circle","geometric","red"]},{"b":"🟠","k":["circle","orange"]},{"b":"🟡","k":["circle","yellow"]},{"b":"🟢","k":["circle","green"]},{"b":"🔵","k":["blue","circle","geometric","large"]},{"b":"🟣","k":["circle","purple"]},{"b":"🟤","k":["brown","circle"]},{"b":"⚫","k":["black","circle","geometric"]},{"b":"⚪","k":["circle","geometric","white"]},{"b":"🟥","k":["card","penalty","red","square"]},{"b":"🟧","k":["orange","square"]},{"b":"🟨","k":["card","penalty","square","yellow"]},{"b":"🟩","k":["green","square"]},{"b":"🟦","k":["blue","square"]},{"b":"🟪","k":["purple","square"]},{"b":"🟫","k":["brown","square"]},{"b":"⬛","k":["black","geometric","large","square"]},{"b":"⬜","k":["geometric","large","square","white"]},{"b":"◼️","k":["black","medium","square"]},{"b":"◻️","k":["medium","square","white"]},{"b":"◾","k":["black","geometric","medium","medium-small","small","square"]},{"b":"◽","k":["geometric","medium","medium-small","small","square","white"]},{"b":"▪️","k":["black","small","square"]},{"b":"▫️","k":["small","square","white"]},{"b":"🔶","k":["diamond","geometric","large","orange"]},{"b":"🔷","k":["blue","diamond","geometric","large"]},{"b":"🔸","k":["diamond","geometric","orange","small"]},{"b":"🔹","k":["blue","diamond","geometric","small"]},{"b":"🔺","k":["geometric","pointed","red","small","triangle","up"]},{"b":"🔻","k":["down","geometric","pointed","red","small","triangle"]},{"b":"💠","k":["a","comic","diamond","dot","geometric","inside","shape","with"]},{"b":"🔘","k":["button","geometric","radio"]},{"b":"🔳","k":["button","geometric","outlined","square","white"]},{"b":"🔲","k":["black","button","geometric","square"]}]},{"name":"Flags","icon":"🏁","emojis":[{"b":"🏁","k":["checkered","chequered","finish","flag","flags","game","milestone","race","racing","sport","win"]},{"b":"🚩","k":["construction","flag","golf","on","post","triangular"]},{"b":"🎌","k":["celebration","cross","crossed","flags","japanese"]},{"b":"🏴","k":["black","flag","waving"]},{"b":"🏳️","k":["flag","white"]},{"b":"🏳️‍🌈","k":["flag","pride","rainbow"]},{"b":"🏳️‍⚧️","k":["flag","transgender"]},{"b":"🏴‍☠️","k":["flag","pirate"]},{"b":"🇦🇨","k":["ascension","flag:","island"]},{"b":"🇦🇩","k":["andorra","flag:"]},{"b":"🇦🇪","k":["arab","emirates","flag:","united"]},{"b":"🇦🇫","k":["afghanistan","flag:"]},{"b":"🇦🇬","k":["antigua","barbuda","flag:"]},{"b":"🇦🇮","k":["anguilla","flag:"]},{"b":"🇦🇱","k":["albania","flag:"]},{"b":"🇦🇲","k":["armenia","flag:"]},{"b":"🇦🇴","k":["angola","flag:"]},{"b":"🇦🇶","k":["antarctica","flag:"]},{"b":"🇦🇷","k":["argentina","flag:"]},{"b":"🇦🇸","k":["american","flag:","samoa"]},{"b":"🇦🇹","k":["austria","flag:"]},{"b":"🇦🇺","k":["australia","flag:"]},{"b":"🇦🇼","k":["aruba","flag:"]},{"b":"🇦🇽","k":["aland","flag:","islands","åland"]},{"b":"🇦🇿","k":["azerbaijan","flag:"]},{"b":"🇧🇦","k":["bosnia","flag:","herzegovina"]},{"b":"🇧🇧","k":["barbados","flag:"]},{"b":"🇧🇩","k":["bangladesh","flag:"]},{"b":"🇧🇪","k":["belgium","flag:"]},{"b":"🇧🇫","k":["burkina","faso","flag:"]},{"b":"🇧🇬","k":["bulgaria","flag:"]},{"b":"🇧🇭","k":["bahrain","flag:"]},{"b":"🇧🇮","k":["burundi","flag:"]},{"b":"🇧🇯","k":["benin","flag:"]},{"b":"🇧🇱","k":["barthelemy","barthélemy","flag:","st","st."]},{"b":"🇧🇲","k":["bermuda","flag:"]},{"b":"🇧🇳","k":["brunei","flag:"]},{"b":"🇧🇴","k":["bolivia","flag:"]},{"b":"🇧🇶","k":["caribbean","flag:","netherlands"]},{"b":"🇧🇷","k":["brazil","flag:"]},{"b":"🇧🇸","k":["bahamas","flag:"]},{"b":"🇧🇹","k":["bhutan","flag:"]},{"b":"🇧🇻","k":["bouvet","flag:","island"]},{"b":"🇧🇼","k":["botswana","flag:"]},{"b":"🇧🇾","k":["belarus","flag:"]},{"b":"🇧🇿","k":["belize","flag:"]},{"b":"🇨🇦","k":["canada","flag:"]},{"b":"🇨🇨","k":["(keeling)","cocos","flag:","islands","keeling"]},{"b":"🇨🇩","k":["congo","flag:","kinshasa"]},{"b":"🇨🇫","k":["african","central","flag:","republic"]},{"b":"🇨🇬","k":["brazzaville","congo","flag:"]},{"b":"🇨🇭","k":["flag:","switzerland"]},{"b":"🇨🇮","k":["cote","côte","divoire","d’ivoire","flag:","ivory"]},{"b":"🇨🇰","k":["cook","flag:","islands"]},{"b":"🇨🇱","k":["chile","flag:"]},{"b":"🇨🇲","k":["cameroon","flag:"]},{"b":"🇨🇳","k":["china","cn","flag:"]},{"b":"🇨🇴","k":["colombia","flag:"]},{"b":"🇨🇵","k":["clipperton","flag:","island"]},{"b":"🇨🇷","k":["costa","flag:","rica"]},{"b":"🇨🇺","k":["cuba","flag:"]},{"b":"🇨🇻","k":["cape","flag:","verde"]},{"b":"🇨🇼","k":["curacao","curaçao","flag:"]},{"b":"🇨🇽","k":["christmas","flag:","island"]},{"b":"🇨🇾","k":["cyprus","flag:"]},{"b":"🇨🇿","k":["czech","czechia","flag:","republic"]},{"b":"🇩🇪","k":["de","flag","flag:","germany"]},{"b":"🇩🇬","k":["diego","flag:","garcia"]},{"b":"🇩🇯","k":["djibouti","flag:"]},{"b":"🇩🇰","k":["denmark","flag:"]},{"b":"🇩🇲","k":["dominica","flag:"]},{"b":"🇩🇴","k":["dominican","flag:","republic"]},{"b":"🇩🇿","k":["algeria","flag:"]},{"b":"🇪🇦","k":["ceuta","flag:","melilla"]},{"b":"🇪🇨","k":["ecuador","flag:"]},{"b":"🇪🇪","k":["estonia","flag:"]},{"b":"🇪🇬","k":["egypt","flag:"]},{"b":"🇪🇭","k":["flag:","sahara","western"]},{"b":"🇪🇷","k":["eritrea","flag:"]},{"b":"🇪🇸","k":["es","flag:","spain"]},{"b":"🇪🇹","k":["ethiopia","flag:"]},{"b":"🇪🇺","k":["eu","european","flag:","union"]},{"b":"🇫🇮","k":["finland","flag:"]},{"b":"🇫🇯","k":["fiji","flag:"]},{"b":"🇫🇰","k":["falkland","flag:","islands"]},{"b":"🇫🇲","k":["flag:","micronesia"]},{"b":"🇫🇴","k":["faroe","flag:","islands"]},{"b":"🇫🇷","k":["flag:","fr","france","french"]},{"b":"🇬🇦","k":["flag:","gabon"]},{"b":"🇬🇧","k":["british","flag","flag:","gb","kingdom","uk","united"]},{"b":"🇬🇩","k":["flag:","grenada"]},{"b":"🇬🇪","k":["flag:","georgia"]},{"b":"🇬🇫","k":["flag:","french","guiana"]},{"b":"🇬🇬","k":["flag:","guernsey"]},{"b":"🇬🇭","k":["flag:","ghana"]},{"b":"🇬🇮","k":["flag:","gibraltar"]},{"b":"🇬🇱","k":["flag:","greenland"]},{"b":"🇬🇲","k":["flag:","gambia"]},{"b":"🇬🇳","k":["flag:","guinea"]},{"b":"🇬🇵","k":["flag:","guadeloupe"]},{"b":"🇬🇶","k":["equatorial","flag:","guinea"]},{"b":"🇬🇷","k":["flag:","greece"]},{"b":"🇬🇸","k":["flag:","georgia","islands","sandwich","south"]},{"b":"🇬🇹","k":["flag:","guatemala"]},{"b":"🇬🇺","k":["flag:","guam"]},{"b":"🇬🇼","k":["bissau","flag:","guinea","guinea-bissau"]},{"b":"🇬🇾","k":["flag:","guyana"]},{"b":"🇭🇰","k":["china","flag:","hong","kong","sar"]},{"b":"🇭🇲","k":["flag:","heard","islands","mcdonald"]},{"b":"🇭🇳","k":["flag:","honduras"]},{"b":"🇭🇷","k":["croatia","flag:"]},{"b":"🇭🇹","k":["flag:","haiti"]},{"b":"🇭🇺","k":["flag:","hungary"]},{"b":"🇮🇨","k":["canary","flag:","islands"]},{"b":"🇮🇩","k":["flag:","indonesia"]},{"b":"🇮🇪","k":["flag:","ireland"]},{"b":"🇮🇱","k":["flag:","israel"]},{"b":"🇮🇲","k":["flag:","isle","man","of"]},{"b":"🇮🇳","k":["flag:","india"]},{"b":"🇮🇴","k":["british","flag:","indian","ocean","territory"]},{"b":"🇮🇶","k":["flag:","iraq"]},{"b":"🇮🇷","k":["flag:","iran"]},{"b":"🇮🇸","k":["flag:","iceland"]},{"b":"🇮🇹","k":["flag:","it","italy"]},{"b":"🇯🇪","k":["flag:","jersey"]},{"b":"🇯🇲","k":["flag:","jamaica"]},{"b":"🇯🇴","k":["flag:","jordan"]},{"b":"🇯🇵","k":["flag:","japan","jp"]},{"b":"🇰🇪","k":["flag:","kenya"]},{"b":"🇰🇬","k":["flag:","kyrgyzstan"]},{"b":"🇰🇭","k":["cambodia","flag:"]},{"b":"🇰🇮","k":["flag:","kiribati"]},{"b":"🇰🇲","k":["comoros","flag:"]},{"b":"🇰🇳","k":["flag:","kitts","nevis","st","st."]},{"b":"🇰🇵","k":["flag:","korea","north"]},{"b":"🇰🇷","k":["flag:","korea","kr","south"]},{"b":"🇰🇼","k":["flag:","kuwait"]},{"b":"🇰🇾","k":["cayman","flag:","islands"]},{"b":"🇰🇿","k":["flag:","kazakhstan"]},{"b":"🇱🇦","k":["flag:","laos"]},{"b":"🇱🇧","k":["flag:","lebanon"]},{"b":"🇱🇨","k":["flag:","lucia","st","st."]},{"b":"🇱🇮","k":["flag:","liechtenstein"]},{"b":"🇱🇰","k":["flag:","lanka","sri"]},{"b":"🇱🇷","k":["flag:","liberia"]},{"b":"🇱🇸","k":["flag:","lesotho"]},{"b":"🇱🇹","k":["flag:","lithuania"]},{"b":"🇱🇺","k":["flag:","luxembourg"]},{"b":"🇱🇻","k":["flag:","latvia"]},{"b":"🇱🇾","k":["flag:","libya"]},{"b":"🇲🇦","k":["flag:","morocco"]},{"b":"🇲🇨","k":["flag:","monaco"]},{"b":"🇲🇩","k":["flag:","moldova"]},{"b":"🇲🇪","k":["flag:","montenegro"]},{"b":"🇲🇫","k":["flag:","martin","st","st."]},{"b":"🇲🇬","k":["flag:","madagascar"]},{"b":"🇲🇭","k":["flag:","islands","marshall"]},{"b":"🇲🇰","k":["flag:","macedonia","north"]},{"b":"🇲🇱","k":["flag:","mali"]},{"b":"🇲🇲","k":["(burma)","burma","flag:","myanmar"]},{"b":"🇲🇳","k":["flag:","mongolia"]},{"b":"🇲🇴","k":["china","flag:","macao","macau","sar"]},{"b":"🇲🇵","k":["flag:","islands","mariana","northern"]},{"b":"🇲🇶","k":["flag:","martinique"]},{"b":"🇲🇷","k":["flag:","mauritania"]},{"b":"🇲🇸","k":["flag:","montserrat"]},{"b":"🇲🇹","k":["flag:","malta"]},{"b":"🇲🇺","k":["flag:","mauritius"]},{"b":"🇲🇻","k":["flag:","maldives"]},{"b":"🇲🇼","k":["flag:","malawi"]},{"b":"🇲🇽","k":["flag:","mexico"]},{"b":"🇲🇾","k":["flag:","malaysia"]},{"b":"🇲🇿","k":["flag:","mozambique"]},{"b":"🇳🇦","k":["flag:","namibia"]},{"b":"🇳🇨","k":["caledonia","flag:","new"]},{"b":"🇳🇪","k":["flag:","niger"]},{"b":"🇳🇫","k":["flag:","island","norfolk"]},{"b":"🇳🇬","k":["flag:","nigeria"]},{"b":"🇳🇮","k":["flag:","nicaragua"]},{"b":"🇳🇱","k":["flag:","netherlands"]},{"b":"🇳🇴","k":["flag:","norway"]},{"b":"🇳🇵","k":["flag:","nepal"]},{"b":"🇳🇷","k":["flag:","nauru"]},{"b":"🇳🇺","k":["flag:","niue"]},{"b":"🇳🇿","k":["flag:","new","zealand"]},{"b":"🇴🇲","k":["flag:","oman"]},{"b":"🇵🇦","k":["flag:","panama"]},{"b":"🇵🇪","k":["flag:","peru"]},{"b":"🇵🇫","k":["flag:","french","polynesia"]},{"b":"🇵🇬","k":["flag:","guinea","new","papua"]},{"b":"🇵🇭","k":["flag:","philippines"]},{"b":"🇵🇰","k":["flag:","pakistan"]},{"b":"🇵🇱","k":["flag:","poland"]},{"b":"🇵🇲","k":["flag:","miquelon","pierre","st","st."]},{"b":"🇵🇳","k":["flag:","islands","pitcairn"]},{"b":"🇵🇷","k":["flag:","puerto","rico"]},{"b":"🇵🇸","k":["flag:","palestinian","territories"]},{"b":"🇵🇹","k":["flag:","portugal"]},{"b":"🇵🇼","k":["flag:","palau"]},{"b":"🇵🇾","k":["flag:","paraguay"]},{"b":"🇶🇦","k":["flag:","qatar"]},{"b":"🇷🇪","k":["flag:","reunion","réunion"]},{"b":"🇷🇴","k":["flag:","romania"]},{"b":"🇷🇸","k":["flag:","serbia"]},{"b":"🇷🇺","k":["flag:","ru","russia"]},{"b":"🇷🇼","k":["flag:","rwanda"]},{"b":"🇸🇦","k":["arabia","flag:","saudi"]},{"b":"🇸🇧","k":["flag:","islands","solomon"]},{"b":"🇸🇨","k":["flag:","seychelles"]},{"b":"🇸🇩","k":["flag:","sudan"]},{"b":"🇸🇪","k":["flag:","sweden"]},{"b":"🇸🇬","k":["flag:","singapore"]},{"b":"🇸🇭","k":["flag:","helena","st","st."]},{"b":"🇸🇮","k":["flag:","slovenia"]},{"b":"🇸🇯","k":["flag:","jan","mayen","svalbard"]},{"b":"🇸🇰","k":["flag:","slovakia"]},{"b":"🇸🇱","k":["flag:","leone","sierra"]},{"b":"🇸🇲","k":["flag:","marino","san"]},{"b":"🇸🇳","k":["flag:","senegal"]},{"b":"🇸🇴","k":["flag:","somalia"]},{"b":"🇸🇷","k":["flag:","suriname"]},{"b":"🇸🇸","k":["flag:","south","sudan"]},{"b":"🇸🇹","k":["flag:","principe","príncipe","sao","são","tome","tomé"]},{"b":"🇸🇻","k":["el","flag:","salvador"]},{"b":"🇸🇽","k":["flag:","maarten","sint"]},{"b":"🇸🇾","k":["flag:","syria"]},{"b":"🇸🇿","k":["eswatini","flag:","swaziland"]},{"b":"🇹🇦","k":["cunha","da","flag:","tristan"]},{"b":"🇹🇨","k":["caicos","flag:","islands","turks"]},{"b":"🇹🇩","k":["chad","flag:"]},{"b":"🇹🇫","k":["flag:","french","southern","territories"]},{"b":"🇹🇬","k":["flag:","togo"]},{"b":"🇹🇭","k":["flag:","thailand"]},{"b":"🇹🇯","k":["flag:","tajikistan"]},{"b":"🇹🇰","k":["flag:","tokelau"]},{"b":"🇹🇱","k":["flag:","leste","timor","timor-leste"]},{"b":"🇹🇲","k":["flag:","turkmenistan"]},{"b":"🇹🇳","k":["flag:","tunisia"]},{"b":"🇹🇴","k":["flag:","tonga"]},{"b":"🇹🇷","k":["flag:","tr","turkey"]},{"b":"🇹🇹","k":["flag:","tobago","trinidad"]},{"b":"🇹🇻","k":["flag:","tuvalu"]},{"b":"🇹🇼","k":["flag:","taiwan"]},{"b":"🇹🇿","k":["flag:","tanzania"]},{"b":"🇺🇦","k":["flag:","ukraine"]},{"b":"🇺🇬","k":["flag:","uganda"]},{"b":"🇺🇲","k":["flag:","islands","outlying","u.s.","us"]},{"b":"🇺🇳","k":["flag:","nations","united"]},{"b":"🇺🇸","k":["america","flag","flag:","states","united","us"]},{"b":"🇺🇾","k":["flag:","uruguay"]},{"b":"🇺🇿","k":["flag:","uzbekistan"]},{"b":"🇻🇦","k":["city","flag:","vatican"]},{"b":"🇻🇨","k":["flag:","grenadines","st","st.","vincent"]},{"b":"🇻🇪","k":["flag:","venezuela"]},{"b":"🇻🇬","k":["british","flag:","islands","virgin"]},{"b":"🇻🇮","k":["flag:","islands","u.s.","us","virgin"]},{"b":"🇻🇳","k":["flag:","vietnam"]},{"b":"🇻🇺","k":["flag:","vanuatu"]},{"b":"🇼🇫","k":["flag:","futuna","wallis"]},{"b":"🇼🇸","k":["flag:","samoa"]},{"b":"🇽🇰","k":["flag:","kosovo"]},{"b":"🇾🇪","k":["flag:","yemen"]},{"b":"🇾🇹","k":["flag:","mayotte"]},{"b":"🇿🇦","k":["africa","flag:","south"]},{"b":"🇿🇲","k":["flag:","zambia"]},{"b":"🇿🇼","k":["flag:","zimbabwe"]},{"b":"🏴󠁧󠁢󠁥󠁮󠁧󠁿","k":["england","flag:"]},{"b":"🏴󠁧󠁢󠁳󠁣󠁴󠁿","k":["flag:","scotland"]},{"b":"🏴󠁧󠁢󠁷󠁬󠁳󠁿","k":["flag:","wales"]}]}]}
\ No newline at end of file

diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/AlertDialogs.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/AlertDialogs.kt
index c167516340..dc22912b44 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/AlertDialogs.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/AlertDialogs.kt
@@ -25,6 +25,7 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@@ -66,6 +67,7 @@ import org.meshtastic.core.ui.util.annotatedStringFromHtml
* @param dismissable Whether the dialog can be dismissed by clicking outside or pressing back.
*/
@Composable
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Suppress("LongMethod", "CyclomaticComplexMethod")
fun MeshtasticDialog(
modifier: Modifier = Modifier,
@@ -137,7 +139,7 @@ fun MeshtasticDialog(
text = titleText,
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
- style = MaterialTheme.typography.titleLarge,
+ style = MaterialTheme.typography.headlineSmallEmphasized,
)
},
text = {
@@ -167,7 +169,7 @@ fun MeshtasticDialog(
}
}
},
- shape = RoundedCornerShape(16.dp),
+ shape = RoundedCornerShape(28.dp),
)
}

diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/BottomSheetDialog.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/BottomSheetDialog.kt
deleted file mode 100644
index 13327e8556..0000000000
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/BottomSheetDialog.kt
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Copyright (c) 2026 Meshtastic LLC
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
-package org.meshtastic.core.ui.component
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.ColumnScope
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.window.Dialog
-import androidx.compose.ui.window.DialogProperties
-
-@Composable
-fun BottomSheetDialog(
- onDismiss: () -> Unit,
- modifier: Modifier = Modifier,
- content: @Composable ColumnScope.() -> Unit,
-) = Dialog(onDismissRequest = onDismiss, properties = DialogProperties(usePlatformDefaultWidth = false)) {
- Box(
- modifier =
- Modifier.fillMaxSize()
- .background(Color.Transparent)
- .clickable(
- onClick = onDismiss,
- indication = null,
- interactionSource = remember { MutableInteractionSource() },
- ),
- ) {
- Column(
- modifier =
- modifier
- .align(Alignment.BottomCenter)
- .background(
- color = MaterialTheme.colorScheme.surface.copy(alpha = 1f),
- shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
- )
- .padding(16.dp),
- content = content,
- )
- }
-}

diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ChannelItem.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ChannelItem.kt
index d037c84a09..7eeaea4448 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ChannelItem.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ChannelItem.kt
@@ -14,6 +14,8 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
+
package org.meshtastic.core.ui.component
import androidx.compose.foundation.clickable
@@ -23,6 +25,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.AssistChip
import androidx.compose.material3.Card
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -54,7 +57,7 @@ fun ChannelItem(
modifier = Modifier.weight(1f),
overflow = TextOverflow.Ellipsis,
maxLines = 1,
- style = MaterialTheme.typography.bodyLarge,
+ style = MaterialTheme.typography.titleMediumEmphasized,
color = fontColor,
)
content()

diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ClickableTextField.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ClickableTextField.kt
deleted file mode 100644
index 4e30b09b54..0000000000
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ClickableTextField.kt
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright (c) 2026 Meshtastic LLC
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
-package org.meshtastic.core.ui.component
-
-import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.interaction.collectIsPressedAsState
-import androidx.compose.material3.Icon
-import androidx.compose.material3.OutlinedTextField
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.vector.ImageVector
-import org.jetbrains.compose.resources.StringResource
-import org.jetbrains.compose.resources.stringResource
-
-@Composable
-fun ClickableTextField(
- label: StringResource,
- enabled: Boolean,
- trailingIcon: ImageVector,
- value: String,
- onClick: () -> Unit,
- modifier: Modifier = Modifier,
- isError: Boolean = false,
- trailingIconContentDescription: String? = null,
-) {
- val source = remember { MutableInteractionSource() }
- val isPressed by source.collectIsPressedAsState()
- if (isPressed) onClick()
-
- OutlinedTextField(
- value,
- onValueChange = {},
- enabled = enabled,
- readOnly = true,
- label = { Text(stringResource(label)) },
- trailingIcon = { Icon(trailingIcon, trailingIconContentDescription) },
- isError = isError,
- interactionSource = source,
- modifier = modifier,
- )
-}

diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ListItem.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ListItem.kt
index e34199ac94..cb4b398d0c 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ListItem.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ListItem.kt
@@ -14,11 +14,12 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
+
package org.meshtastic.core.ui.component
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.size
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
@@ -29,6 +30,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.Clipboard
import androidx.compose.ui.platform.LocalClipboard
@@ -49,14 +51,14 @@ fun ListItem(
text: String,
modifier: Modifier = Modifier,
supportingText: String? = null,
- textColor: Color = LocalContentColor.current,
- supportingTextColor: Color = LocalContentColor.current,
+ textColor: Color = Color.Unspecified,
+ supportingTextColor: Color = Color.Unspecified,
copyable: Boolean = false,
enabled: Boolean = true,
leadingIcon: ImageVector? = null,
- leadingIconTint: Color = LocalContentColor.current,
+ leadingIconTint: Color = Color.Unspecified,
trailingIcon: ImageVector? = MeshtasticIcons.ChevronRight,
- trailingIconTint: Color = LocalContentColor.current,
+ trailingIconTint: Color = Color.Unspecified,
onClick: (() -> Unit)? = null,
) {
val clipboard: Clipboard = LocalClipboard.current
@@ -89,10 +91,10 @@ fun SwitchListItem(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
- textColor: Color = LocalContentColor.current,
+ textColor: Color = Color.Unspecified,
enabled: Boolean = true,
leadingIcon: ImageVector? = null,
- leadingIconTint: Color = LocalContentColor.current,
+ leadingIconTint: Color = Color.Unspecified,
) {
BasicListItem(
text = text,
@@ -113,42 +115,57 @@ fun SwitchListItem(
* This is a core component that should facilitate most list item use cases. Please carefully consider if modifying this
* is really necessary before doing so.
*
+ * Uses the M3 Expressive interactive [ListItem] overload which provides built-in shape morphing on press/hover and
+ * proper disabled styling.
+ *
* @see [LinkedCoordinatesItem] for example usage
*/
@Composable
fun BasicListItem(
text: String,
modifier: Modifier = Modifier,
- textColor: Color = LocalContentColor.current,
+ textColor: Color = Color.Unspecified,
supportingText: String? = null,
- supportingTextColor: Color = LocalContentColor.current,
+ supportingTextColor: Color = Color.Unspecified,
enabled: Boolean = true,
leadingIcon: ImageVector? = null,
- leadingIconTint: Color = LocalContentColor.current,
+ leadingIconTint: Color = Color.Unspecified,
trailingContent: @Composable (() -> Unit)? = null,
onClick: (() -> Unit)? = null,
onLongClick: (() -> Unit)? = null,
) {
- ListItem(
- modifier =
- if (onLongClick != null) {
- modifier.combinedClickable(enabled = enabled, onLongClick = onLongClick, onClick = onClick ?: {})
- } else if (onClick != null) {
- modifier.clickable(enabled = enabled, onClick = onClick)
- } else {
- modifier
- },
- colors = ListItemDefaults.colors(containerColor = Color.Transparent),
- headlineContent = { Text(text = text, color = textColor) },
- supportingContent = supportingText?.let { { Text(text = it, color = supportingTextColor) } },
- leadingContent = leadingIcon.icon(leadingIconTint),
- trailingContent = trailingContent,
- )
+ if (onClick != null) {
+ ListItem(
+ onClick = onClick,
+ modifier = modifier,
+ enabled = enabled,
+ onLongClick = onLongClick,
+ shapes = ListItemDefaults.shapes(shape = RectangleShape),
+ colors = ListItemDefaults.colors(containerColor = Color.Transparent),
+ leadingContent = leadingIcon.icon(leadingIconTint),
+ trailingContent = trailingContent,
+ supportingContent = supportingText?.let { { Text(text = it, color = supportingTextColor) } },
+ content = { Text(text = text, color = textColor) },
+ )
+ } else {
+ ListItem(
+ modifier = modifier,
+ colors = ListItemDefaults.colors(containerColor = Color.Transparent),
+ headlineContent = { Text(text = text, color = textColor) },
+ supportingContent = supportingText?.let { { Text(text = it, color = supportingTextColor) } },
+ leadingContent = leadingIcon.icon(leadingIconTint),
+ trailingContent = trailingContent,
+ )
+ }
}
@Composable
-fun ImageVector?.icon(tint: Color = LocalContentColor.current): @Composable (() -> Unit)? =
- this?.let { { Icon(imageVector = it, contentDescription = null, modifier = Modifier.size(24.dp), tint = tint) } }
+fun ImageVector?.icon(tint: Color = Color.Unspecified): @Composable (() -> Unit)? = this?.let {
+ {
+ val resolvedTint = if (tint == Color.Unspecified) LocalContentColor.current else tint
+ Icon(imageVector = it, contentDescription = null, modifier = Modifier.size(24.dp), tint = resolvedTint)
+ }
+}
@Preview(showBackground = true)
@Composable

diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MainAppBar.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MainAppBar.kt
index 41b6b8a3d8..4df0c8b5a2 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MainAppBar.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MainAppBar.kt
@@ -74,7 +74,7 @@ fun MainAppBar(
text = title,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
- style = MaterialTheme.typography.titleLarge,
+ style = MaterialTheme.typography.titleLargeEmphasized,
)
},
subtitle = {

diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MenuFAB.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MenuFAB.kt
index db82fe7fe0..163d754431 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MenuFAB.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MenuFAB.kt
@@ -16,13 +16,19 @@
*/
package org.meshtastic.core.ui.component
+import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FloatingActionButtonMenu
import androidx.compose.material3.FloatingActionButtonMenuItem
import androidx.compose.material3.Icon
+import androidx.compose.material3.PlainTooltip
import androidx.compose.material3.Text
import androidx.compose.material3.ToggleFloatingActionButton
import androidx.compose.material3.ToggleFloatingActionButtonDefaults
+import androidx.compose.material3.TooltipAnchorPosition
+import androidx.compose.material3.TooltipBox
+import androidx.compose.material3.TooltipDefaults
+import androidx.compose.material3.rememberTooltipState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -32,7 +38,7 @@ import org.meshtastic.core.ui.icon.Close
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.OfflineShare
-@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
@Composable
fun MenuFAB(
expanded: Boolean,
@@ -46,15 +52,21 @@ fun MenuFAB(
modifier = modifier.then(if (testTag != null) Modifier.testTag(testTag) else Modifier),
expanded = expanded,
button = {
- ToggleFloatingActionButton(
- checked = expanded,
- onCheckedChange = onExpandedChange,
- content = {
- val imageVector = if (expanded) MeshtasticIcons.Close else MeshtasticIcons.OfflineShare
- Icon(imageVector = imageVector, contentDescription = contentDescription)
- },
- containerColor = ToggleFloatingActionButtonDefaults.containerColor(),
- )
+ TooltipBox(
+ positionProvider = TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
+ tooltip = { contentDescription?.let { PlainTooltip { Text(it) } } },
+ state = rememberTooltipState(),
+ ) {
+ ToggleFloatingActionButton(
+ checked = expanded,
+ onCheckedChange = onExpandedChange,
+ content = {
+ val imageVector = if (expanded) MeshtasticIcons.Close else MeshtasticIcons.OfflineShare
+ Icon(imageVector = imageVector, contentDescription = contentDescription)
+ },
+ containerColor = ToggleFloatingActionButtonDefaults.containerColor(),
+ )
+ }
},
horizontalAlignment = Alignment.End,
) {

diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticNavigationSuite.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticNavigationSuite.kt
index de477fc4a5..5fa55b0403 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticNavigationSuite.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticNavigationSuite.kt
@@ -26,6 +26,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Badge
import androidx.compose.material3.BadgedBox
import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme.colorScheme
@@ -68,7 +69,7 @@ import org.meshtastic.core.ui.viewmodel.UIViewModel
* This implementation uses the [MultiBackstack] state holder to manage independent histories for each tab, aligning
* with Navigation 3 best practices for state preservation during tab switching.
*/
-@OptIn(ExperimentalMaterial3Api::class)
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun MeshtasticNavigationSuite(
multiBackstack: MultiBackstack,

diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/RegularPreference.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/RegularPreference.kt
index 8427a5392e..013bc3f0b3 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/RegularPreference.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/RegularPreference.kt
@@ -14,27 +14,26 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
+
package org.meshtastic.core.ui.component
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.ExperimentalLayoutApi
-import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentWidth
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
+import androidx.compose.material3.ListItem
+import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@@ -63,7 +62,6 @@ fun RegularPreference(
)
}
-@OptIn(ExperimentalLayoutApi::class)
@Composable
fun RegularPreference(
title: String,
@@ -75,51 +73,30 @@ fun RegularPreference(
trailingIcon: ImageVector? = null,
dropdownMenu: @Composable () -> Unit = {},
) {
- val color =
- if (enabled) {
- MaterialTheme.colorScheme.onSurface
- } else {
- MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
- }
-
- Column(
- modifier =
- modifier
- .fillMaxWidth()
- .clickable(enabled = enabled, onClick = onClick, role = Role.Button)
- .padding(all = 16.dp),
- ) {
- Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween) {
- FlowRow(modifier = Modifier.weight(1f), horizontalArrangement = Arrangement.SpaceBetween) {
- Text(
- text = title,
- style = MaterialTheme.typography.bodyLarge,
- color =
- if (enabled) {
- Color.Unspecified
- } else {
- MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
- },
- )
-
- Text(text = subtitle, style = MaterialTheme.typography.bodyLarge, color = color)
- }
- if (trailingIcon != null) {
- Box {
- Icon(
- imageVector = trailingIcon,
- contentDescription = null,
- modifier = Modifier.padding(start = 8.dp).wrapContentWidth(Alignment.End),
- tint = color,
- )
- dropdownMenu()
+ ListItem(
+ onClick = onClick,
+ modifier = modifier,
+ enabled = enabled,
+ shapes = ListItemDefaults.shapes(shape = RectangleShape),
+ colors = ListItemDefaults.colors(containerColor = Color.Transparent),
+ trailingContent = {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Text(text = subtitle, style = MaterialTheme.typography.bodyLarge)
+ if (trailingIcon != null) {
+ Box {
+ Icon(
+ imageVector = trailingIcon,
+ contentDescription = null,
+ modifier = Modifier.padding(start = 8.dp).wrapContentWidth(Alignment.End),
+ )
+ dropdownMenu()
+ }
}
}
- }
- if (summary != null) {
- Text(text = summary, style = MaterialTheme.typography.bodyMedium, color = color)
- }
- }
+ },
+ supportingContent = summary?.let { { Text(text = it, style = MaterialTheme.typography.bodyMedium) } },
+ content = { Text(text = title, style = MaterialTheme.typography.bodyLarge) },
+ )
}
@Preview(showBackground = true)

diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/SliderPreference.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/SliderPreference.kt
index e47071aeaf..d27a0cb562 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/SliderPreference.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/SliderPreference.kt
@@ -16,6 +16,9 @@
*/
package org.meshtastic.core.ui.component
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.core.spring
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@@ -23,6 +26,7 @@ import androidx.compose.material3.ListItem
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
@@ -46,6 +50,15 @@ fun <T> SliderPreference(
val valueRange = 0f..(items.size - 1).toFloat()
val steps = (items.size - 2).coerceAtLeast(0)
+ // Spring-animated thumb position for expressive feel
+ val animatedValue by
+ animateFloatAsState(
+ targetValue = selectedIndex.coerceIn(valueRange),
+ animationSpec =
+ spring(stiffness = Spring.StiffnessMediumLow, dampingRatio = Spring.DampingRatioMediumBouncy),
+ label = "sliderSpring",
+ )
+
ListItem(
modifier = modifier,
headlineContent = {
@@ -60,7 +73,7 @@ fun <T> SliderPreference(
Column {
summary?.let { Text(text = it, modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp)) }
Slider(
- value = selectedIndex.coerceIn(valueRange),
+ value = animatedValue,
onValueChange = {
val index = it.roundToInt()
if (index in items.indices) {

diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/SlidingSelector.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/SlidingSelector.kt
deleted file mode 100644
index 9664e0cd13..0000000000
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/SlidingSelector.kt
+++ /dev/null
@@ -1,389 +0,0 @@
-/*
- * Copyright (c) 2026 Meshtastic LLC
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
-package org.meshtastic.core.ui.component
-
-import androidx.compose.animation.core.animateDpAsState
-import androidx.compose.animation.core.animateFloatAsState
-import androidx.compose.foundation.Canvas
-import androidx.compose.foundation.background
-import androidx.compose.foundation.gestures.awaitEachGesture
-import androidx.compose.foundation.gestures.awaitFirstDown
-import androidx.compose.foundation.gestures.horizontalDrag
-import androidx.compose.foundation.layout.Arrangement.spacedBy
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.wrapContentWidth
-import androidx.compose.foundation.selection.selectableGroup
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material3.LocalTextStyle
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableFloatStateOf
-import androidx.compose.runtime.mutableIntStateOf
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
-import androidx.compose.ui.draw.alpha
-import androidx.compose.ui.draw.shadow
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.TransformOrigin
-import androidx.compose.ui.graphics.graphicsLayer
-import androidx.compose.ui.input.pointer.AwaitPointerEventScope
-import androidx.compose.ui.input.pointer.PointerEventPass
-import androidx.compose.ui.input.pointer.PointerInputChange
-import androidx.compose.ui.input.pointer.changedToUp
-import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.layout.Layout
-import androidx.compose.ui.semantics.Role
-import androidx.compose.ui.semantics.onClick
-import androidx.compose.ui.semantics.role
-import androidx.compose.ui.semantics.selected
-import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.semantics.stateDescription
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.style.TextOverflow.Companion.Ellipsis
-import androidx.compose.ui.unit.Constraints
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.dp
-
-private const val NO_OPTION_INDEX = -1
-
-private val TRACK_PADDING = 2.dp
-
-private val TRACK_COLOR = Color.LightGray.copy(alpha = .5f)
-
-private val PRESSED_TRACK_PADDING = 1.dp
-
-private val OPTION_PADDING = 5.dp
-
-private const val PRESSED_UNSELECTED_ALPHA = .6f
-
-private val BACKGROUND_SHAPE = RoundedCornerShape(8.dp)
-
-/**
- * Provides the user with a set of options they can choose from.
- *
- * (Inspired by https://gist.github.com/zach-klippenstein/7ae8874db304f957d6bb91263e292117)
- */
-@Composable
-fun <T : Any> SlidingSelector(
- options: List<T>,
- selectedOption: T,
- onOptionSelected: (T) -> Unit,
- modifier: Modifier = Modifier,
- content: @Composable (T) -> Unit,
-) {
- val state = remember { SelectorState() }
- state.optionCount = options.size
- state.selectedOption = options.indexOf(selectedOption)
- state.onOptionSelected = { onOptionSelected(options[it]) }
-
- /* Animate between whole-number indices so we don't need to do pixel calculations. */
- val selectedIndexOffset by animateFloatAsState(state.selectedOption.toFloat(), label = "Selected Index Offset")
-
- Layout(
- content = {
- SelectedIndicator(state)
- Dividers(state)
- Options(state, options, content)
- },
- modifier =
- modifier
- .fillMaxWidth()
- .then(state.inputModifier)
- .background(TRACK_COLOR, BACKGROUND_SHAPE)
- .padding(TRACK_PADDING),
- ) { measurables, constraints ->
- val (indicatorMeasurable, dividersMeasurable, optionsMeasurable) = measurables
-
- /* Measure the options first so we know how tall to make the indicator. */
- val optionsPlaceable = optionsMeasurable.measure(constraints)
- state.updatePressedScale(optionsPlaceable.height, this)
-
- /* Measure the indicator and dividers to be the right size. */
- val indicatorPlaceable =
- indicatorMeasurable.measure(
- Constraints.fixed(width = optionsPlaceable.width / options.size, height = optionsPlaceable.height),
- )
-
- val dividersPlaceable =
- dividersMeasurable.measure(
- Constraints.fixed(width = optionsPlaceable.width, height = optionsPlaceable.height),
- )
-
- layout(optionsPlaceable.width, optionsPlaceable.height) {
- val optionWidth = optionsPlaceable.width / options.size
-
- /* Place the indicator first so that it's below the option labels. */
- indicatorPlaceable.placeRelative(x = (selectedIndexOffset * optionWidth).toInt(), y = 0)
- dividersPlaceable.placeRelative(IntOffset.Zero)
- optionsPlaceable.placeRelative(IntOffset.Zero)
- }
- }
-}
-
-/** Visual representation of the option the user may select. */
-@Composable
-fun OptionLabel(text: String) {
- Text(text, maxLines = 1, overflow = Ellipsis)
-}
-
-/** Draws the selected indicator on the [SlidingSelector] track. */
-@Composable
-private fun SelectedIndicator(state: SelectorState) {
- Box(
- Modifier.then(
- state.optionScaleModifier(
- pressed = state.pressedOption == state.selectedOption,
- option = state.selectedOption,
- ),
- )
- .shadow(4.dp, BACKGROUND_SHAPE)
- .background(MaterialTheme.colorScheme.background, BACKGROUND_SHAPE),
- )
-}
-
-/** Draws dividers between [OptionLabel]s. */
-@Composable
-private fun Dividers(state: SelectorState) {
- /* Animate each divider independently. */
- val alphas =
- (0 until state.optionCount).map { i ->
- val selectionAdjacent = i == state.selectedOption || i - 1 == state.selectedOption
- animateFloatAsState(if (selectionAdjacent) 0f else 1f, label = "Dividers")
- }
-
- Canvas(Modifier.fillMaxSize()) {
- val optionWidth = size.width / state.optionCount
- val dividerPadding = TRACK_PADDING + PRESSED_TRACK_PADDING
-
- alphas.forEachIndexed { i, alpha ->
- val x = i * optionWidth
- drawLine(
- Color.White,
- alpha = alpha.value,
- start = Offset(x, dividerPadding.toPx()),
- end = Offset(x, size.height - dividerPadding.toPx()),
- )
- }
- }
-}
-
-/** Draws the options available to the user. */
-@Composable
-private fun <T> Options(state: SelectorState, options: List<T>, content: @Composable (T) -> Unit) {
- CompositionLocalProvider(LocalTextStyle provides TextStyle(fontWeight = FontWeight.Medium)) {
- Row(horizontalArrangement = spacedBy(TRACK_PADDING), modifier = Modifier.fillMaxWidth().selectableGroup()) {
- options.forEachIndexed { i, timeFrame ->
- val isSelected = i == state.selectedOption
- val isPressed = i == state.pressedOption
-
- /* Unselected presses are represented by fading. */
- val alpha by
- animateFloatAsState(
- if (!isSelected && isPressed) PRESSED_UNSELECTED_ALPHA else 1f,
- label = "Unselected",
- )
-
- val semanticsModifier =
- Modifier.semantics(mergeDescendants = true) {
- selected = isSelected
- role = Role.Button
- onClick {
- state.onOptionSelected(i)
- true
- }
- stateDescription = if (isSelected) "Selected" else "Not selected"
- }
-
- Box(
- Modifier
- /* Divide space evenly between all options. */
- .weight(1f)
- .then(semanticsModifier)
- .padding(OPTION_PADDING)
- /* Draw pressed indication when not selected. */
- .alpha(alpha)
- /* Selected presses are represented by scaling. */
- .then(state.optionScaleModifier(isPressed && isSelected, i))
- /* Center the option content. */
- .wrapContentWidth(),
- ) {
- content(timeFrame)
- }
- }
- }
- }
-}
-
-/** Contains and handles the state necessary to present the [SlidingSelector] to the user. */
-private class SelectorState {
- var optionCount by mutableIntStateOf(0)
- var selectedOption by mutableIntStateOf(0)
- var onOptionSelected: (Int) -> Unit by mutableStateOf({})
- var pressedOption by mutableIntStateOf(NO_OPTION_INDEX)
-
- /**
- * Scale factor that should be used to scale pressed option. When this scale is applied, exactly
- * [PRESSED_TRACK_PADDING] will be added around the element's usual size.
- */
- var pressedSelectedScale by mutableFloatStateOf(1f)
- private set
-
- /** Calculates the scale factor we need to use for pressed options to get the desired padding. */
- fun updatePressedScale(controlHeight: Int, density: Density) {
- with(density) {
- val pressedPadding = PRESSED_TRACK_PADDING * 2
- val pressedHeight = controlHeight - pressedPadding.toPx()
- pressedSelectedScale = pressedHeight / controlHeight
- }
- }
-
- /**
- * Returns a [Modifier] that will scale an element so that it gets [PRESSED_TRACK_PADDING] extra padding around it.
- * The scale will be animated.
- *
- * The scale is also performed around either the left or right edge of the element if the option is the first or
- * last option, respectively. In those cases, the scale will also be translated so that [PRESSED_TRACK_PADDING] will
- * be added on the left or right edge.
- */
- fun optionScaleModifier(pressed: Boolean, option: Int): Modifier = Modifier.composed {
- val scale by animateFloatAsState(if (pressed) pressedSelectedScale else 1f, label = "Scale")
- val xOffset by animateDpAsState(if (pressed) PRESSED_TRACK_PADDING else 0.dp, label = "x Offset")
-
- graphicsLayer {
- this.scaleX = scale
- this.scaleY = scale
-
- /* Scales on the ends should gravitate to that edge. */
- this.transformOrigin =
- TransformOrigin(
- pivotFractionX =
- when (option) {
- 0 -> 0f
- optionCount - 1 -> 1f
- else -> .5f
- },
- pivotFractionY = .5f,
- )
-
- /* But should still move inwards to keep the pressed padding consistent with top and bottom. */
- this.translationX =
- when (option) {
- 0 -> xOffset.toPx()
- optionCount - 1 -> -xOffset.toPx()
- else -> 0f
- }
- }
- }
-
- /**
- * A [Modifier] that will listen for touch gestures and update the selected and pressed properties of this state
- * appropriately.
- */
- val inputModifier =
- Modifier.pointerInput(optionCount) {
- val optionWidth = size.width / optionCount
-
- /* Helper to calculate which option an event occurred in. */
- fun optionIndex(change: PointerInputChange): Int =
- ((change.position.x / size.width.toFloat()) * optionCount).toInt().coerceIn(0, optionCount - 1)
-
- awaitEachGesture {
- val down = awaitFirstDown()
-
- pressedOption = optionIndex(down)
- val downOnSelected = pressedOption == selectedOption
- val optionBounds =
- Rect(
- left = pressedOption * optionWidth.toFloat(),
- right = (pressedOption + 1) * optionWidth.toFloat(),
- top = 0f,
- bottom = size.height.toFloat(),
- )
-
- if (downOnSelected) {
- horizontalDrag(down.id) { change ->
- pressedOption = optionIndex(change)
-
- if (pressedOption != selectedOption) {
- onOptionSelected(pressedOption)
- }
- }
- } else {
- waitForUpOrCancellation(inBounds = optionBounds)
- /* Null means the gesture was cancelled (e.g. dragged out of bounds). */
- ?.let { onOptionSelected(pressedOption) }
- }
- pressedOption = NO_OPTION_INDEX
- }
- }
-}
-
-/** Works with bounds that may not be at 0,0. */
-@Suppress("ReturnCount")
-private suspend fun AwaitPointerEventScope.waitForUpOrCancellation(inBounds: Rect): PointerInputChange? {
- while (true) {
- val event = awaitPointerEvent(PointerEventPass.Main)
- if (event.changes.all { it.changedToUp() }) {
- /* All pointers are up */
- return event.changes[0]
- }
-
- if (event.changes.any { it.isConsumed || !inBounds.contains(it.position) }) {
- /* Canceled */
- return null
- }
-
- val consumeCheck = awaitPointerEvent(PointerEventPass.Final)
- if (consumeCheck.changes.any { it.isConsumed }) {
- return null
- }
- }
-}
-
-/*@Preview
-@Composable
-fun SlidingSelectorPreview() {
- MaterialTheme {
- Surface {
- Column(Modifier.padding(8.dp)) {
-
- var selectedOption by remember { mutableStateOf(TimeFrame.TWENTY_FOUR_HOURS) }
- SlidingSelector(
- TimeFrame.entries.toList(),
- selectedOption,
- onOptionSelected = { selectedOption = it }
- ) {
- OptionLabel(stringResource(it.strRes))
- }
- }
- }
- }
-}*/

diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/SwitchPreference.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/SwitchPreference.kt
index fc7764fefa..005653e305 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/SwitchPreference.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/SwitchPreference.kt
@@ -14,14 +14,16 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
+
package org.meshtastic.core.ui.component
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.selection.toggleable
-import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.CircularWavyProgressIndicator
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.Switch
@@ -29,6 +31,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.meshtastic.core.ui.theme.AppTheme
@@ -45,32 +48,19 @@ fun SwitchPreference(
containerColor: Color? = null,
loading: Boolean = false,
) {
- val defaultColors = ListItemDefaults.colors()
-
- @Suppress("DEPRECATION")
- val currentColors =
- if (enabled) {
- defaultColors
- } else {
- defaultColors.copy(
- headlineColor = defaultColors.headlineColor.copy(alpha = 0.5f),
- supportingTextColor = defaultColors.supportingTextColor.copy(alpha = 0.5f),
- )
- }
- .let { if (containerColor != null) it.copy(containerColor = containerColor) else it }
+ val currentColors = ListItemDefaults.colors(containerColor = containerColor ?: Color.Unspecified)
ListItem(
+ checked = checked,
+ onCheckedChange = onCheckedChange,
+ modifier = padding?.let { Modifier.padding(it) } ?: modifier,
+ enabled = enabled,
+ shapes = ListItemDefaults.shapes(shape = RectangleShape),
colors = currentColors,
- modifier =
- (padding?.let { Modifier.padding(it) } ?: modifier).toggleable(
- value = checked,
- enabled = enabled,
- onValueChange = onCheckedChange,
- ),
trailingContent = {
AnimatedContent(targetState = loading) { loading ->
if (loading) {
- CircularProgressIndicator(modifier = Modifier.size(24.dp))
+ CircularWavyProgressIndicator(modifier = Modifier.size(24.dp))
} else {
Switch(enabled = enabled, checked = checked, onCheckedChange = null)
}
@@ -81,7 +71,7 @@ fun SwitchPreference(
Text(text = summary)
}
},
- headlineContent = { Text(text = title) },
+ content = { Text(text = title) },
)
}

diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/TimeTickWithLifecycle.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/TimeTickWithLifecycle.kt
deleted file mode 100644
index 5678453142..0000000000
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/TimeTickWithLifecycle.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * Copyright (c) 2026 Meshtastic LLC
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
-package org.meshtastic.core.ui.component
-
-import androidx.compose.runtime.Composable
-
-/**
- * Remembers a time tick that updates every minute.
- *
- * @return The current time in milliseconds, updating every minute.
- */
-@Composable expect fun rememberTimeTickWithLifecycle(): Long

diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/emoji/EmojiData.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/emoji/EmojiData.kt
deleted file mode 100644
index 8e7f65c34f..0000000000
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/emoji/EmojiData.kt
+++ /dev/null
@@ -1,2009 +0,0 @@
-/*
- * Copyright (c) 2026 Meshtastic LLC
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
-@file:Suppress("LongMethod", "MaxLineLength", "StringLiteralDuplication", "TooManyFunctions")
-
-package org.meshtastic.core.ui.emoji
-
-/** A single emoji entry with optional skin-tone support and search keywords. */
-internal data class Emoji(
- val base: String,
- val keywords: List<String> = emptyList(),
- val supportsSkinTone: Boolean = false,
-)
-
-/** A named category of emojis with an icon emoji for the tab. */
-internal data class EmojiCategory(val name: String, val icon: String, val emojis: List<Emoji>)
-
-/** Unicode skin tone modifiers (Fitzpatrick scale). */
-internal enum class SkinTone(val modifier: String, val label: String, val preview: String) {
- DEFAULT("", "Default", "👋"),
- LIGHT("\uD83C\uDFFB", "Light", "👋🏻"),
- MEDIUM_LIGHT("\uD83C\uDFFC", "Medium-Light", "👋🏼"),
- MEDIUM("\uD83C\uDFFD", "Medium", "👋🏽"),
- MEDIUM_DARK("\uD83C\uDFFE", "Medium-Dark", "👋🏾"),
- DARK("\uD83C\uDFFF", "Dark", "👋🏿"),
-}
-
-/**
- * Applies a skin tone modifier to a base emoji string. Only works correctly for single-codepoint emojis that support
- * skin tones.
- */
-internal fun Emoji.withSkinTone(tone: SkinTone): String {
- if (!supportsSkinTone || tone == SkinTone.DEFAULT) return base
- // Insert the modifier after the first code point (which may be a surrogate pair)
- val firstChar = base[0]
- val charCount = if (firstChar.isHighSurrogate() && base.length > 1) 2 else 1
- val baseChar = base.substring(0, charCount)
- val after = base.substring(charCount)
- return baseChar + tone.modifier + after
-}
-
-// ── Emoji Catalog ──────────────────────────────────────────────────────────────
-
-@Suppress("LargeClass", "MaxLineLength", "LongMethod")
-internal object EmojiData {
- private fun e(base: String, vararg kw: String, skin: Boolean = false) = Emoji(base, kw.toList(), skin)
-
- val categories: List<EmojiCategory> =
- listOf(smileys(), people(), nature(), food(), travel(), activities(), objects(), symbols(), flags())
-
- /** Flat list for search. */
- val all: List<Emoji> by lazy { categories.flatMap { it.emojis } }
-
- // ── Categories ─────────────────────────────────────────────────────────────
- private fun smileys() = EmojiCategory(
- name = "Smileys & Emotion",
- icon = "😀",
- emojis =
- listOf(
- e("😀", "grinning", "face", "smile", "happy"),
- e("😃", "grinning", "face", "with", "big", "eyes", "smiley", "happy", "joy", "haha"),
- e("😄", "grinning", "face", "with", "smiling", "eyes", "smile", "happy", "joy", "laugh", "pleased"),
- e("😁", "beaming", "face", "with", "smiling", "eyes", "grin"),
- e("😆", "grinning", "squinting", "face", "laughing", "satisfied", "happy", "haha"),
- e("😅", "grinning", "face", "with", "sweat", "smile", "hot"),
- e("🤣", "rolling", "on", "the", "floor", "laughing", "rofl", "lol"),
- e("😂", "face", "with", "tears", "of", "joy"),
- e("🙂", "slightly", "smiling", "face"),
- e("🙃", "upside", "down", "face"),
- e("🫠", "melting", "face", "sarcasm", "dread"),
- e("😉", "winking", "face", "wink", "flirt"),
- e("😊", "smiling", "face", "with", "eyes", "blush", "proud"),
- e("😇", "smiling", "face", "with", "halo", "innocent", "angel"),
- e("🥰", "smiling", "face", "with", "hearts", "three", "love"),
- e("😍", "smiling", "face", "with", "heart", "eyes", "love", "crush"),
- e("🤩", "star", "struck", "eyes"),
- e("😘", "face", "blowing", "a", "kiss", "kissing", "heart", "flirt"),
- e("😗", "kissing", "face"),
- e("☺️", "smiling", "face", "relaxed", "blush", "pleased"),
- e("😚", "kissing", "face", "with", "closed", "eyes"),
- e("😙", "kissing", "face", "with", "smiling", "eyes"),
- e("🥲", "smiling", "face", "with", "tear"),
- e("😋", "face", "savoring", "food", "yum", "tongue", "lick"),
- e("😛", "face", "with", "tongue", "stuck", "out"),
- e("😜", "winking", "face", "with", "tongue", "stuck", "out", "eye", "prank", "silly"),
- e("🤪", "zany", "face", "goofy", "wacky"),
- e("😝", "squinting", "face", "with", "tongue", "stuck", "out", "closed", "eyes", "prank"),
- e("🤑", "money", "mouth", "face", "rich"),
- e("🤗", "smiling", "face", "with", "open", "hands", "hugs"),
- e("🤭", "face", "with", "hand", "over", "mouth", "quiet", "whoops"),
- e("🫢", "face", "with", "open", "eyes", "and", "hand", "over", "mouth", "gasp", "shock"),
- e("🫣", "face", "with", "peeking", "eye"),
- e("🤫", "shushing", "face", "silence", "quiet"),
- e("🤔", "thinking", "face"),
- e("🫡", "saluting", "face", "respect"),
- e("🤐", "zipper", "mouth", "face", "silence", "hush"),
- e("🤨", "face", "with", "raised", "eyebrow", "suspicious"),
- e("😐", "neutral", "face", "meh"),
- e("😑", "expressionless", "face"),
- e("😶", "face", "without", "mouth", "no", "mute", "silence"),
- e("🫥", "dotted", "line", "face", "invisible"),
- e("😶‍🌫️", "face", "in", "clouds"),
- e("😏", "smirking", "face", "smirk", "smug"),
- e("😒", "unamused", "face", "meh"),
- e("🙄", "face", "with", "rolling", "eyes", "roll"),
- e("😬", "grimacing", "face"),
- e("😮‍💨", "face", "exhaling"),
- e("🤥", "lying", "face", "liar"),
- e("🫨", "shaking", "face", "shock"),
- e("😌", "relieved", "face", "whew"),
- e("😔", "pensive", "face"),
- e("😪", "sleepy", "face", "tired"),
- e("🤤", "drooling", "face"),
- e("😴", "sleeping", "face", "zzz"),
- e("😷", "face", "with", "medical", "mask", "sick", "ill"),
- e("🤒", "face", "with", "thermometer", "sick"),
- e("🤕", "face", "with", "head", "bandage", "hurt"),
- e("🤢", "nauseated", "face", "sick", "barf", "disgusted"),
- e("🤮", "face", "vomiting", "barf", "sick"),
- e("🤧", "sneezing", "face", "achoo", "sick"),
- e("🥵", "hot", "face", "heat", "sweating"),
- e("🥶", "cold", "face", "freezing", "ice"),
- e("🥴", "woozy", "face", "groggy"),
- e("😵", "face", "with", "crossed", "out", "eyes", "dizzy"),
- e("😵‍💫", "face", "with", "spiral", "eyes"),
- e("🤯", "exploding", "head", "mind", "blown"),
- e("🤠", "cowboy", "hat", "face"),
- e("🥳", "partying", "face", "celebration", "birthday"),
- e("🥸", "disguised", "face"),
- e("😎", "smiling", "face", "with", "sunglasses", "cool"),
- e("🤓", "nerd", "face", "geek", "glasses"),
- e("🧐", "face", "with", "monocle"),
- e("😕", "confused", "face"),
- e("🫤", "face", "with", "diagonal", "mouth", "confused"),
- e("😟", "worried", "face", "nervous"),
- e("🙁", "slightly", "frowning", "face"),
- e("☹️", "frowning", "face"),
- e("😮", "face", "with", "open", "mouth", "surprise", "impressed", "wow"),
- e("😯", "hushed", "face", "silence", "speechless"),
- e("😲", "astonished", "face", "amazed", "gasp"),
- e("😳", "flushed", "face"),
- e("🥺", "pleading", "face", "puppy", "eyes"),
- e("🥹", "face", "holding", "back", "tears", "gratitude"),
- e("😦", "frowning", "face", "with", "open", "mouth"),
- e("😧", "anguished", "face", "stunned"),
- e("😨", "fearful", "face", "scared", "shocked", "oops"),
- e("😰", "anxious", "face", "with", "sweat", "cold", "nervous"),
- e("😥", "sad", "but", "relieved", "face", "disappointed", "phew", "sweat", "nervous"),
- e("😢", "crying", "face", "cry", "sad", "tear"),
- e("😭", "loudly", "crying", "face", "sob", "sad", "cry", "bawling"),
- e("😱", "face", "screaming", "in", "fear", "scream", "horror", "shocked"),
- e("😖", "confounded", "face"),
- e("😣", "persevering", "face", "persevere", "struggling"),
- e("😞", "disappointed", "face", "sad"),
- e("😓", "downcast", "face", "with", "sweat"),
- e("😩", "weary", "face", "tired"),
- e("😫", "tired", "face", "upset", "whine"),
- e("🥱", "yawning", "face"),
- e("😤", "face", "with", "steam", "from", "nose", "triumph", "smug"),
- e("😡", "enraged", "face", "rage", "pout", "angry"),
- e("😠", "angry", "face", "mad", "annoyed"),
- e("🤬", "face", "with", "symbols", "on", "mouth", "cursing", "foul"),
- e("😈", "smiling", "face", "with", "horns", "imp", "devil", "evil"),
- e("👿", "angry", "face", "with", "horns", "imp", "devil", "evil"),
- e("💀", "skull", "dead", "danger", "poison"),
- e("☠️", "skull", "and", "crossbones", "danger", "pirate"),
- e("💩", "pile", "of", "poo", "hankey", "poop", "shit", "crap"),
- e("🤡", "clown", "face"),
- e("👹", "ogre", "japanese", "monster"),
- e("👺", "goblin", "japanese"),
- e("👻", "ghost", "halloween"),
- e("👽", "alien", "ufo"),
- e("👾", "alien", "monster", "space", "invader", "game", "retro"),
- e("🤖", "robot"),
- e("😺", "grinning", "cat", "smiley"),
- e("😸", "grinning", "cat", "with", "smiling", "eyes", "smile"),
- e("😹", "cat", "with", "tears", "of", "joy"),
- e("😻", "smiling", "cat", "with", "heart", "eyes"),
- e("😼", "cat", "with", "wry", "smile", "smirk"),
- e("😽", "kissing", "cat"),
- e("🙀", "weary", "cat", "scream", "horror"),
- e("😿", "crying", "cat", "face", "sad", "tear"),
- e("😾", "pouting", "cat"),
- e("🙈", "see", "no", "evil", "monkey", "blind", "ignore"),
- e("🙉", "hear", "no", "evil", "monkey", "deaf"),
- e("🙊", "speak", "no", "evil", "monkey", "mute", "hush"),
- e("💌", "love", "letter", "email", "envelope"),
- e("💘", "heart", "with", "arrow", "cupid", "love"),
- e("💝", "heart", "with", "ribbon", "gift", "chocolates"),
- e("💖", "sparkling", "heart"),
- e("💗", "growing", "heart", "heartpulse"),
- e("💓", "beating", "heart", "heartbeat"),
- e("💞", "revolving", "hearts"),
- e("💕", "two", "hearts"),
- e("💟", "heart", "decoration"),
- e("❣️", "heart", "exclamation", "heavy"),
- e("💔", "broken", "heart"),
- e("❤️‍🔥", "heart", "on", "fire"),
- e("❤️‍🩹", "mending", "heart"),
- e("❤️", "red", "heart", "love"),
- e("🩷", "pink", "heart"),
- e("🧡", "orange", "heart"),
- e("💛", "yellow", "heart"),
- e("💚", "green", "heart"),
- e("💙", "blue", "heart"),
- e("🩵", "light", "blue", "heart"),
- e("💜", "purple", "heart"),
- e("🤎", "brown", "heart"),
- e("🖤", "black", "heart"),
- e("🩶", "grey", "heart"),
- e("🤍", "white", "heart"),
- e("💋", "kiss", "mark", "lipstick"),
- e("💯", "hundred", "points", "100", "score", "perfect"),
- e("💢", "anger", "symbol", "angry"),
- e("💥", "collision", "boom", "explode"),
- e("💫", "dizzy", "star"),
- e("💦", "sweat", "droplets", "drops", "water", "workout"),
- e("💨", "dashing", "away", "dash", "wind", "blow", "fast"),
- e("🕳️", "hole"),
- e("💬", "speech", "balloon", "comment"),
- e("👁️‍🗨️", "eye", "in", "speech", "bubble"),
- e("🗨️", "left", "speech", "bubble"),
- e("🗯️", "right", "anger", "bubble"),
- e("💭", "thought", "balloon", "thinking"),
- e("💤", "zzz", "sleeping"),
- ),
- )
-
- private fun people() = EmojiCategory(
- name = "People & Body",
- icon = "👋",
- emojis =
- listOf(
- e("👋", "waving", "hand", "wave", "goodbye", skin = true),
- e("🤚", "raised", "back", "of", "hand", skin = true),
- e("🖐️", "hand", "with", "fingers", "splayed", "raised", skin = true),
- e("✋", "raised", "hand", "highfive", "stop", skin = true),
- e("🖖", "vulcan", "salute", "prosper", "spock", skin = true),
- e("🫱", "rightwards", "hand", skin = true),
- e("🫲", "leftwards", "hand", skin = true),
- e("🫳", "palm", "down", "hand", skin = true),
- e("🫴", "palm", "up", "hand", skin = true),
- e("🫷", "leftwards", "pushing", "hand", skin = true),
- e("🫸", "rightwards", "pushing", "hand", skin = true),
- e("👌", "ok", "hand", skin = true),
- e("🤌", "pinched", "fingers", skin = true),
- e("🤏", "pinching", "hand", skin = true),
- e("✌️", "victory", "hand", "v", "peace", skin = true),
- e("🤞", "crossed", "fingers", "luck", "hopeful", skin = true),
- e("🫰", "hand", "with", "index", "finger", "and", "thumb", "crossed", skin = true),
- e("🤟", "love", "you", "gesture", skin = true),
- e("🤘", "sign", "of", "the", "horns", "metal", skin = true),
- e("🤙", "call", "me", "hand", skin = true),
- e("👈", "backhand", "index", "pointing", "left", "point", skin = true),
- e("👉", "backhand", "index", "pointing", "right", "point", skin = true),
- e("👆", "backhand", "index", "pointing", "up", "point", "2", skin = true),
- e("🖕", "middle", "finger", "fu", skin = true),
- e("👇", "backhand", "index", "pointing", "down", "point", skin = true),
- e("☝️", "index", "pointing", "up", "point", skin = true),
- e("🫵", "index", "pointing", "at", "the", "viewer", skin = true),
- e("👍", "thumbs", "up", "thumbsup", "approve", "ok", skin = true),
- e("👎", "thumbs", "down", "thumbsdown", "disapprove", "bury", skin = true),
- e("✊", "raised", "fist", "power", skin = true),
- e("👊", "oncoming", "fist", "facepunch", "punch", "attack", skin = true),
- e("🤛", "left", "facing", "fist", skin = true),
- e("🤜", "right", "facing", "fist", skin = true),
- e("👏", "clapping", "hands", "clap", "praise", "applause", skin = true),
- e("🙌", "raising", "hands", "raised", "hooray", skin = true),
- e("🫶", "heart", "hands", "love", skin = true),
- e("👐", "open", "hands", skin = true),
- e("🤲", "palms", "up", "together", skin = true),
- e("🤝", "handshake", "deal", skin = true),
- e("🙏", "folded", "hands", "pray", "please", "hope", "wish", skin = true),
- e("✍️", "writing", "hand", skin = true),
- e("💅", "nail", "polish", "care", "beauty", "manicure", skin = true),
- e("🤳", "selfie", skin = true),
- e("💪", "flexed", "biceps", "muscle", "flex", "bicep", "strong", "workout", skin = true),
- e("🦾", "mechanical", "arm"),
- e("🦿", "mechanical", "leg"),
- e("🦵", "leg", skin = true),
- e("🦶", "foot", skin = true),
- e("👂", "ear", "hear", "sound", "listen", skin = true),
- e("🦻", "ear", "with", "hearing", "aid", skin = true),
- e("👃", "nose", "smell", skin = true),
- e("🧠", "brain"),
- e("🫀", "anatomical", "heart"),
- e("🫁", "lungs"),
- e("🦷", "tooth"),
- e("🦴", "bone"),
- e("👀", "eyes", "look", "see", "watch"),
- e("👁️", "eye"),
- e("👅", "tongue", "taste"),
- e("👄", "mouth", "lips", "kiss"),
- e("🫦", "biting", "lip"),
- e("👶", "baby", "child", "newborn", skin = true),
- e("🧒", "child", skin = true),
- e("👦", "boy", "child", skin = true),
- e("👧", "girl", "child", skin = true),
- e("🧑", "person", "adult", skin = true),
- e("👱", "blond", "hair", "haired", "person", skin = true),
- e("👨", "man", "mustache", "father", "dad", skin = true),
- e("🧔", "beard", "bearded", "person", skin = true),
- e("🧔‍♂️", "beard", "man", skin = true),
- e("🧔‍♀️", "beard", "woman", skin = true),
- e("👨‍🦰", "red", "hair", "haired", "man", skin = true),
- e("👨‍🦱", "curly", "hair", "haired", "man", skin = true),
- e("👨‍🦳", "white", "hair", "haired", "man", skin = true),
- e("👨‍🦲", "bald", "man", skin = true),
- e("👩", "woman", "girls", skin = true),
- e("👩‍🦰", "red", "hair", "haired", "woman", skin = true),
- e("🧑‍🦰", "red", "hair", "person", skin = true),
- e("👩‍🦱", "curly", "hair", "haired", "woman", skin = true),
- e("🧑‍🦱", "curly", "hair", "person", skin = true),
- e("👩‍🦳", "white", "hair", "haired", "woman", skin = true),
- e("🧑‍🦳", "white", "hair", "person", skin = true),
- e("👩‍🦲", "bald", "woman", skin = true),
- e("🧑‍🦲", "bald", "person", skin = true),
- e("👱‍♀️", "blond", "hair", "haired", "woman", "blonde", skin = true),
- e("👱‍♂️", "blond", "hair", "haired", "man", skin = true),
- e("🧓", "older", "person", "adult", skin = true),
- e("👴", "old", "man", "older", skin = true),
- e("👵", "old", "woman", "older", skin = true),
- e("🙍", "person", "frowning", skin = true),
- e("🙍‍♂️", "man", "frowning", skin = true),
- e("🙍‍♀️", "woman", "frowning", skin = true),
- e("🙎", "person", "pouting", "face", skin = true),
- e("🙎‍♂️", "man", "pouting", skin = true),
- e("🙎‍♀️", "woman", "pouting", skin = true),
- e("🙅", "person", "gesturing", "no", "good", "stop", "halt", "denied", skin = true),
- e("🙅‍♂️", "man", "gesturing", "no", "good", "ng", "stop", "halt", "denied", skin = true),
- e("🙅‍♀️", "woman", "gesturing", "no", "good", "ng", "stop", "halt", "denied", skin = true),
- e("🙆", "person", "gesturing", "ok", skin = true),
- e("🙆‍♂️", "man", "gesturing", "ok", skin = true),
- e("🙆‍♀️", "woman", "gesturing", "ok", skin = true),
- e("💁", "person", "tipping", "hand", "information", "desk", skin = true),
- e("💁‍♂️", "man", "tipping", "hand", "sassy", "information", skin = true),
- e("💁‍♀️", "woman", "tipping", "hand", "sassy", "information", skin = true),
- e("🙋", "person", "raising", "hand", skin = true),
- e("🙋‍♂️", "man", "raising", "hand", skin = true),
- e("🙋‍♀️", "woman", "raising", "hand", skin = true),
- e("🧏", "deaf", "person", skin = true),
- e("🧏‍♂️", "deaf", "man", skin = true),
- e("🧏‍♀️", "deaf", "woman", skin = true),
- e("🙇", "person", "bowing", "bow", "respect", "thanks", skin = true),
- e("🙇‍♂️", "man", "bowing", "respect", "thanks", skin = true),
- e("🙇‍♀️", "woman", "bowing", "respect", "thanks", skin = true),
- e("🤦", "person", "facepalming", "facepalm", skin = true),
- e("🤦‍♂️", "man", "facepalming", skin = true),
- e("🤦‍♀️", "woman", "facepalming", skin = true),
- e("🤷", "person", "shrugging", "shrug", skin = true),
- e("🤷‍♂️", "man", "shrugging", skin = true),
- e("🤷‍♀️", "woman", "shrugging", skin = true),
- e("🧑‍⚕️", "health", "worker", skin = true),
- e("👨‍⚕️", "man", "health", "worker", "doctor", "nurse", skin = true),
- e("👩‍⚕️", "woman", "health", "worker", "doctor", "nurse", skin = true),
- e("🧑‍🎓", "student", skin = true),
- e("👨‍🎓", "man", "student", "graduation", skin = true),
- e("👩‍🎓", "woman", "student", "graduation", skin = true),
- e("🧑‍🏫", "teacher", skin = true),
- e("👨‍🏫", "man", "teacher", "school", "professor", skin = true),
- e("👩‍🏫", "woman", "teacher", "school", "professor", skin = true),
- e("🧑‍⚖️", "judge", skin = true),
- e("👨‍⚖️", "man", "judge", "justice", skin = true),
- e("👩‍⚖️", "woman", "judge", "justice", skin = true),
- e("🧑‍🌾", "farmer", skin = true),
- e("👨‍🌾", "man", "farmer", skin = true),
- e("👩‍🌾", "woman", "farmer", skin = true),
- e("🧑‍🍳", "cook", skin = true),
- e("👨‍🍳", "man", "cook", "chef", skin = true),
- e("👩‍🍳", "woman", "cook", "chef", skin = true),
- e("🧑‍🔧", "mechanic", skin = true),
- e("👨‍🔧", "man", "mechanic", skin = true),
- e("👩‍🔧", "woman", "mechanic", skin = true),
- e("🧑‍🏭", "factory", "worker", skin = true),
- e("👨‍🏭", "man", "factory", "worker", skin = true),
- e("👩‍🏭", "woman", "factory", "worker", skin = true),
- e("🧑‍💼", "office", "worker", skin = true),
- e("👨‍💼", "man", "office", "worker", "business", skin = true),
- e("👩‍💼", "woman", "office", "worker", "business", skin = true),
- e("🧑‍🔬", "scientist", skin = true),
- e("👨‍🔬", "man", "scientist", "research", skin = true),
- e("👩‍🔬", "woman", "scientist", "research", skin = true),
- e("🧑‍💻", "technologist", skin = true),
- e("👨‍💻", "man", "technologist", "coder", skin = true),
- e("👩‍💻", "woman", "technologist", "coder", skin = true),
- e("🧑‍🎤", "singer", skin = true),
- e("👨‍🎤", "man", "singer", "rockstar", skin = true),
- e("👩‍🎤", "woman", "singer", "rockstar", skin = true),
- e("🧑‍🎨", "artist", skin = true),
- e("👨‍🎨", "man", "artist", "painter", skin = true),
- e("👩‍🎨", "woman", "artist", "painter", skin = true),
- e("🧑‍✈️", "pilot", skin = true),
- e("👨‍✈️", "man", "pilot", skin = true),
- e("👩‍✈️", "woman", "pilot", skin = true),
- e("🧑‍🚀", "astronaut", skin = true),
- e("👨‍🚀", "man", "astronaut", "space", skin = true),
- e("👩‍🚀", "woman", "astronaut", "space", skin = true),
- e("🧑‍🚒", "firefighter", skin = true),
- e("👨‍🚒", "man", "firefighter", skin = true),
- e("👩‍🚒", "woman", "firefighter", skin = true),
- e("👮", "police", "officer", "cop", "law", skin = true),
- e("👮‍♂️", "man", "police", "officer", "policeman", "law", "cop", skin = true),
- e("👮‍♀️", "woman", "police", "officer", "policewoman", "law", "cop", skin = true),
- e("🕵️", "detective", "sleuth", skin = true),
- e("🕵️‍♂️", "man", "detective", "male", "sleuth", skin = true),
- e("🕵️‍♀️", "woman", "detective", "female", "sleuth", skin = true),
- e("💂", "guard", skin = true),
- e("💂‍♂️", "man", "guard", "guardsman", skin = true),
- e("💂‍♀️", "woman", "guard", "guardswoman", skin = true),
- e("🥷", "ninja", skin = true),
- e("👷", "construction", "worker", "helmet", skin = true),
- e("👷‍♂️", "man", "construction", "worker", "helmet", skin = true),
- e("👷‍♀️", "woman", "construction", "worker", "helmet", skin = true),
- e("🫅", "person", "with", "crown", skin = true),
- e("🤴", "prince", "crown", "royal", skin = true),
- e("👸", "princess", "crown", "royal", skin = true),
- e("👳", "person", "wearing", "turban", "with", skin = true),
- e("👳‍♂️", "man", "wearing", "turban", "with", skin = true),
- e("👳‍♀️", "woman", "wearing", "turban", "with", skin = true),
- e("👲", "person", "with", "skullcap", "man", "gua", "pi", "mao", skin = true),
- e("🧕", "woman", "with", "headscarf", "hijab", skin = true),
- e("🤵", "person", "in", "tuxedo", "groom", "marriage", "wedding", skin = true),
- e("🤵‍♂️", "man", "in", "tuxedo", skin = true),
- e("🤵‍♀️", "woman", "in", "tuxedo", skin = true),
- e("👰", "person", "with", "veil", "marriage", "wedding", skin = true),
- e("👰‍♂️", "man", "with", "veil", skin = true),
- e("👰‍♀️", "woman", "with", "veil", "bride", skin = true),
- e("🤰", "pregnant", "woman", skin = true),
- e("🫃", "pregnant", "man", skin = true),
- e("🫄", "pregnant", "person", skin = true),
- e("🤱", "breast", "feeding", "nursing", skin = true),
- e("👩‍🍼", "woman", "feeding", "baby", skin = true),
- e("👨‍🍼", "man", "feeding", "baby", skin = true),
- e("🧑‍🍼", "person", "feeding", "baby", skin = true),
- e("👼", "baby", "angel", skin = true),
- e("🎅", "santa", "claus", "christmas", skin = true),
- e("🤶", "claus", "mrs", "santa", skin = true),
- e("🧑‍🎄", "mx", "claus", skin = true),
- e("🦸", "superhero", skin = true),
- e("🦸‍♂️", "man", "superhero", skin = true),
- e("🦸‍♀️", "woman", "superhero", skin = true),
- e("🦹", "supervillain", skin = true),
- e("🦹‍♂️", "man", "supervillain", skin = true),
- e("🦹‍♀️", "woman", "supervillain", skin = true),
- e("🧙", "mage", "wizard", skin = true),
- e("🧙‍♂️", "man", "mage", "wizard", skin = true),
- e("🧙‍♀️", "woman", "mage", "wizard", skin = true),
- e("🧚", "fairy", skin = true),
- e("🧚‍♂️", "man", "fairy", skin = true),
- e("🧚‍♀️", "woman", "fairy", skin = true),
- e("🧛", "vampire", skin = true),
- e("🧛‍♂️", "man", "vampire", skin = true),
- e("🧛‍♀️", "woman", "vampire", skin = true),
- e("🧜", "merperson", skin = true),
- e("🧜‍♂️", "merman", skin = true),
- e("🧜‍♀️", "mermaid", skin = true),
- e("🧝", "elf", skin = true),
- e("🧝‍♂️", "man", "elf", skin = true),
- e("🧝‍♀️", "woman", "elf", skin = true),
- e("🧞", "genie"),
- e("🧞‍♂️", "man", "genie"),
- e("🧞‍♀️", "woman", "genie"),
- e("🧟", "zombie"),
- e("🧟‍♂️", "man", "zombie"),
- e("🧟‍♀️", "woman", "zombie"),
- e("🧌", "troll"),
- e("💆", "person", "getting", "massage", "spa", skin = true),
- e("💆‍♂️", "man", "getting", "massage", "spa", skin = true),
- e("💆‍♀️", "woman", "getting", "massage", "spa", skin = true),
- e("💇", "person", "getting", "haircut", "beauty", skin = true),
- e("💇‍♂️", "man", "getting", "haircut", skin = true),
- e("💇‍♀️", "woman", "getting", "haircut", skin = true),
- e("🚶", "person", "walking", skin = true),
- e("🚶‍♂️", "man", "walking", skin = true),
- e("🚶‍♀️", "woman", "walking", skin = true),
- e("🧍", "person", "standing", skin = true),
- e("🧍‍♂️", "man", "standing", skin = true),
- e("🧍‍♀️", "woman", "standing", skin = true),
- e("🧎", "person", "kneeling", skin = true),
- e("🧎‍♂️", "man", "kneeling", skin = true),
- e("🧎‍♀️", "woman", "kneeling", skin = true),
- e("🧑‍🦯", "person", "with", "white", "cane", "probing", skin = true),
- e("👨‍🦯", "man", "with", "white", "cane", "probing", skin = true),
- e("👩‍🦯", "woman", "with", "white", "cane", "probing", skin = true),
- e("🧑‍🦼", "person", "in", "motorized", "wheelchair", skin = true),
- e("👨‍🦼", "man", "in", "motorized", "wheelchair", skin = true),
- e("👩‍🦼", "woman", "in", "motorized", "wheelchair", skin = true),
- e("🧑‍🦽", "person", "in", "manual", "wheelchair", skin = true),
- e("👨‍🦽", "man", "in", "manual", "wheelchair", skin = true),
- e("👩‍🦽", "woman", "in", "manual", "wheelchair", skin = true),
- e("🏃", "person", "running", "runner", "exercise", "workout", "marathon", skin = true),
- e("🏃‍♂️", "man", "running", "exercise", "workout", "marathon", skin = true),
- e("🏃‍♀️", "woman", "running", "exercise", "workout", "marathon", skin = true),
- e("💃", "woman", "dancing", "dancer", "dress", skin = true),
- e("🕺", "man", "dancing", "dancer", skin = true),
- e("🕴️", "person", "in", "suit", "levitating", "business", skin = true),
- e("👯", "people", "with", "bunny", "ears", "dancers"),
- e("👯‍♂️", "men", "with", "bunny", "ears", "dancing"),
- e("👯‍♀️", "women", "with", "bunny", "ears", "dancing"),
- e("🧖", "person", "in", "steamy", "room", "sauna", skin = true),
- e("🧖‍♂️", "man", "in", "steamy", "room", "sauna", skin = true),
- e("🧖‍♀️", "woman", "in", "steamy", "room", "sauna", skin = true),
- e("🧗", "person", "climbing", "bouldering", skin = true),
- e("🧗‍♂️", "man", "climbing", "bouldering", skin = true),
- e("🧗‍♀️", "woman", "climbing", "bouldering", skin = true),
- e("🤺", "person", "fencing"),
- e("🏇", "horse", "racing", skin = true),
- e("⛷️", "skier"),
- e("🏂", "snowboarder", skin = true),
- e("🏌️", "person", "golfing", skin = true),
- e("🏌️‍♂️", "man", "golfing", skin = true),
- e("🏌️‍♀️", "woman", "golfing", skin = true),
- e("🏄", "person", "surfing", "surfer", skin = true),
- e("🏄‍♂️", "man", "surfing", skin = true),
- e("🏄‍♀️", "woman", "surfing", skin = true),
- e("🚣", "person", "rowing", "boat", "rowboat", skin = true),
- e("🚣‍♂️", "man", "rowing", "boat", skin = true),
- e("🚣‍♀️", "woman", "rowing", "boat", skin = true),
- e("🏊", "person", "swimming", "swimmer", skin = true),
- e("🏊‍♂️", "man", "swimming", skin = true),
- e("🏊‍♀️", "woman", "swimming", skin = true),
- e("⛹️", "person", "bouncing", "ball", "basketball", skin = true),
- e("⛹️‍♂️", "man", "bouncing", "ball", "basketball", skin = true),
- e("⛹️‍♀️", "woman", "bouncing", "ball", "basketball", skin = true),
- e("🏋️", "person", "lifting", "weights", "weight", "gym", "workout", skin = true),
- e("🏋️‍♂️", "man", "lifting", "weights", "weight", "gym", "workout", skin = true),
- e("🏋️‍♀️", "woman", "lifting", "weights", "weight", "gym", "workout", skin = true),
- e("🚴", "person", "biking", "bicyclist", skin = true),
- e("🚴‍♂️", "man", "biking", skin = true),
- e("🚴‍♀️", "woman", "biking", skin = true),
- e("🚵", "person", "mountain", "biking", "bicyclist", skin = true),
- e("🚵‍♂️", "man", "mountain", "biking", skin = true),
- e("🚵‍♀️", "woman", "mountain", "biking", skin = true),
- e("🤸", "person", "cartwheeling", skin = true),
- e("🤸‍♂️", "man", "cartwheeling", skin = true),
- e("🤸‍♀️", "woman", "cartwheeling", skin = true),
- e("🤼", "people", "wrestling"),
- e("🤼‍♂️", "men", "wrestling"),
- e("🤼‍♀️", "women", "wrestling"),
- e("🤽", "person", "playing", "water", "polo", skin = true),
- e("🤽‍♂️", "man", "playing", "water", "polo", skin = true),
- e("🤽‍♀️", "woman", "playing", "water", "polo", skin = true),
- e("🤾", "person", "playing", "handball", skin = true),
- e("🤾‍♂️", "man", "playing", "handball", skin = true),
- e("🤾‍♀️", "woman", "playing", "handball", skin = true),
- e("🤹", "person", "juggling", skin = true),
- e("🤹‍♂️", "man", "juggling", skin = true),
- e("🤹‍♀️", "woman", "juggling", skin = true),
- e("🧘", "person", "in", "lotus", "position", "meditation", skin = true),
- e("🧘‍♂️", "man", "in", "lotus", "position", "meditation", skin = true),
- e("🧘‍♀️", "woman", "in", "lotus", "position", "meditation", skin = true),
- e("🛀", "person", "taking", "bath", "shower", skin = true),
- e("🛌", "person", "in", "bed", "sleeping", skin = true),
- e("🧑‍🤝‍🧑", "people", "holding", "hands", "couple", "date", skin = true),
- e("👭", "women", "holding", "hands", "two", "couple", "date", skin = true),
- e("👫", "woman", "and", "man", "holding", "hands", "couple", "date", skin = true),
- e("👬", "men", "holding", "hands", "two", "couple", "date", skin = true),
- e("💏", "kiss", "couplekiss", skin = true),
- e("👩‍❤️‍💋‍👨", "man", "couplekiss", "woman", skin = true),
- e("👨‍❤️‍💋‍👨", "man", "couplekiss", skin = true),
- e("👩‍❤️‍💋‍👩", "woman", "couplekiss", skin = true),
- e("💑", "couple", "with", "heart", skin = true),
- e("👩‍❤️‍👨", "couple", "with", "man", "heart", "woman", skin = true),
- e("👨‍❤️‍👨", "couple", "with", "man", "heart", skin = true),
- e("👩‍❤️‍👩", "couple", "with", "woman", "heart", skin = true),
- e("👪", "family", "home", "parents", "child"),
- e("👨‍👩‍👦", "boy", "family", "man", "woman"),
- e("👨‍👩‍👧", "girl", "family", "man", "woman"),
- e("👨‍👩‍👧‍👦", "boy", "family", "man", "woman", "girl"),
- e("👨‍👩‍👦‍👦", "boy", "family", "man", "woman"),
- e("👨‍👩‍👧‍👧", "girl", "family", "man", "woman"),
- e("👨‍👨‍👦", "boy", "family", "man"),
- e("👨‍👨‍👧", "girl", "family", "man"),
- e("👨‍👨‍👧‍👦", "boy", "family", "man", "girl"),
- e("👨‍👨‍👦‍👦", "boy", "family", "man"),
- e("👨‍👨‍👧‍👧", "girl", "family", "man"),
- e("👩‍👩‍👦", "boy", "family", "woman"),
- e("👩‍👩‍👧", "girl", "family", "woman"),
- e("👩‍👩‍👧‍👦", "boy", "family", "woman", "girl"),
- e("👩‍👩‍👦‍👦", "boy", "family", "woman"),
- e("👩‍👩‍👧‍👧", "girl", "family", "woman"),
- e("👨‍👦", "boy", "family", "man"),
- e("👨‍👦‍👦", "boy", "family", "man"),
- e("👨‍👧", "girl", "family", "man"),
- e("👨‍👧‍👦", "boy", "family", "man", "girl"),
- e("👨‍👧‍👧", "girl", "family", "man"),
- e("👩‍👦", "boy", "family", "woman"),
- e("👩‍👦‍👦", "boy", "family", "woman"),
- e("👩‍👧", "girl", "family", "woman"),
- e("👩‍👧‍👦", "boy", "family", "woman", "girl"),
- e("👩‍👧‍👧", "girl", "family", "woman"),
- e("🗣️", "speaking", "head"),
- e("👤", "bust", "in", "silhouette", "user"),
- e("👥", "busts", "in", "silhouette", "users", "group", "team"),
- e("🫂", "people", "hugging"),
- e("👣", "footprints", "feet", "tracks"),
- ),
- )
-
- private fun nature() = EmojiCategory(
- name = "Animals & Nature",
- icon = "🐾",
- emojis =
- listOf(
- e("🐵", "monkey", "face"),
- e("🐒", "monkey"),
- e("🦍", "gorilla"),
- e("🦧", "orangutan"),
- e("🐶", "dog", "face", "pet"),
- e("🐕", "dog", "dog2"),
- e("🦮", "guide", "dog"),
- e("🐕‍🦺", "service", "dog"),
- e("🐩", "poodle", "dog"),
- e("🐺", "wolf"),
- e("🦊", "fox", "face"),
- e("🦝", "raccoon"),
- e("🐱", "cat", "face", "pet"),
- e("🐈", "cat", "cat2"),
- e("🐈‍⬛", "black", "cat"),
- e("🦁", "lion"),
- e("🐯", "tiger", "face"),
- e("🐅", "tiger", "tiger2"),
- e("🐆", "leopard"),
- e("🐴", "horse", "face"),
- e("🫎", "moose", "canada"),
- e("🫏", "donkey", "mule"),
- e("🐎", "horse", "racehorse", "speed"),
- e("🦄", "unicorn"),
- e("🦓", "zebra"),
- e("🦌", "deer"),
- e("🦬", "bison"),
- e("🐮", "cow", "face"),
- e("🐂", "ox"),
- e("🐃", "water", "buffalo"),
- e("🐄", "cow", "cow2"),
- e("🐷", "pig", "face"),
- e("🐖", "pig", "pig2"),
- e("🐗", "boar"),
- e("🐽", "pig", "nose"),
- e("🐏", "ram"),
- e("🐑", "ewe", "sheep"),
- e("🐐", "goat"),
- e("🐪", "camel", "dromedary", "desert"),
- e("🐫", "two", "hump", "camel"),
- e("🦙", "llama"),
- e("🦒", "giraffe"),
- e("🐘", "elephant"),
- e("🦣", "mammoth"),
- e("🦏", "rhinoceros"),
- e("🦛", "hippopotamus"),
- e("🐭", "mouse", "face"),
- e("🐁", "mouse", "mouse2"),
- e("🐀", "rat"),
- e("🐹", "hamster", "pet"),
- e("🐰", "rabbit", "face", "bunny"),
- e("🐇", "rabbit", "rabbit2"),
- e("🐿️", "chipmunk"),
- e("🦫", "beaver"),
- e("🦔", "hedgehog"),
- e("🦇", "bat"),
- e("🐻", "bear"),
- e("🐻‍❄️", "polar", "bear"),
- e("🐨", "koala"),
- e("🐼", "panda", "face"),
- e("🦥", "sloth"),
- e("🦦", "otter"),
- e("🦨", "skunk"),
- e("🦘", "kangaroo"),
- e("🦡", "badger"),
- e("🐾", "paw", "prints", "feet"),
- e("🦃", "turkey", "thanksgiving"),
- e("🐔", "chicken"),
- e("🐓", "rooster"),
- e("🐣", "hatching", "chick"),
- e("🐤", "baby", "chick"),
- e("🐥", "front", "facing", "baby", "chick", "hatched"),
- e("🐦", "bird"),
- e("🐧", "penguin"),
- e("🕊️", "dove", "peace"),
- e("🦅", "eagle"),
- e("🦆", "duck"),
- e("🦢", "swan"),
- e("🦉", "owl"),
- e("🦤", "dodo"),
- e("🪶", "feather"),
- e("🦩", "flamingo"),
- e("🦚", "peacock"),
- e("🦜", "parrot"),
- e("🪽", "wing", "fly"),
- e("🐦‍⬛", "black", "bird"),
- e("🪿", "goose", "honk"),
- e("🐸", "frog"),
- e("🐊", "crocodile"),
- e("🐢", "turtle", "slow"),
- e("🦎", "lizard"),
- e("🐍", "snake"),
- e("🐲", "dragon", "face"),
- e("🐉", "dragon"),
- e("🦕", "sauropod", "dinosaur"),
- e("🦖", "t", "rex", "dinosaur"),
- e("🐳", "spouting", "whale", "sea"),
- e("🐋", "whale", "whale2"),
- e("🐬", "dolphin", "flipper"),
- e("🦭", "seal"),
- e("🐟", "fish"),
- e("🐠", "tropical", "fish"),
- e("🐡", "blowfish"),
- e("🦈", "shark"),
- e("🐙", "octopus"),
- e("🐚", "spiral", "shell", "sea", "beach"),
- e("🪸", "coral"),
- e("🪼", "jellyfish"),
- e("🐌", "snail", "slow"),
- e("🦋", "butterfly"),
- e("🐛", "bug"),
- e("🐜", "ant"),
- e("🐝", "honeybee", "bee"),
- e("🪲", "beetle"),
- e("🐞", "lady", "beetle", "bug"),
- e("🦗", "cricket"),
- e("🪳", "cockroach"),
- e("🕷️", "spider"),
- e("🕸️", "spider", "web"),
- e("🦂", "scorpion"),
- e("🦟", "mosquito"),
- e("🪰", "fly"),
- e("🪱", "worm"),
- e("🦠", "microbe", "germ"),
- e("💐", "bouquet", "flowers"),
- e("🌸", "cherry", "blossom", "flower", "spring"),
- e("💮", "white", "flower"),
- e("🪷", "lotus"),
- e("🏵️", "rosette"),
- e("🌹", "rose", "flower"),
- e("🥀", "wilted", "flower"),
- e("🌺", "hibiscus"),
- e("🌻", "sunflower"),
- e("🌼", "blossom"),
- e("🌷", "tulip", "flower"),
- e("🪻", "hyacinth"),
- e("🌱", "seedling", "plant"),
- e("🪴", "potted", "plant"),
- e("🌲", "evergreen", "tree", "wood"),
- e("🌳", "deciduous", "tree", "wood"),
- e("🌴", "palm", "tree"),
- e("🌵", "cactus"),
- e("🌾", "sheaf", "of", "rice", "ear"),
- e("🌿", "herb"),
- e("☘️", "shamrock"),
- e("🍀", "four", "leaf", "clover", "luck"),
- e("🍁", "maple", "leaf", "canada"),
- e("🍂", "fallen", "leaf", "autumn"),
- e("🍃", "leaf", "fluttering", "in", "wind", "leaves"),
- e("🪹", "empty", "nest"),
- e("🪺", "nest", "with", "eggs"),
- e("🍄", "mushroom", "fungus"),
- ),
- )
-
- private fun food() = EmojiCategory(
- name = "Food & Drink",
- icon = "🍔",
- emojis =
- listOf(
- e("🍇", "grapes"),
- e("🍈", "melon"),
- e("🍉", "watermelon"),
- e("🍊", "tangerine", "orange", "mandarin"),
- e("🍋", "lemon"),
- e("🍌", "banana", "fruit"),
- e("🍍", "pineapple"),
- e("🥭", "mango"),
- e("🍎", "red", "apple"),
- e("🍏", "green", "apple", "fruit"),
- e("🍐", "pear"),
- e("🍑", "peach"),
- e("🍒", "cherries", "fruit"),
- e("🍓", "strawberry", "fruit"),
- e("🫐", "blueberries"),
- e("🥝", "kiwi", "fruit"),
- e("🍅", "tomato"),
- e("🫒", "olive"),
- e("🥥", "coconut"),
- e("🥑", "avocado"),
- e("🍆", "eggplant", "aubergine"),
- e("🥔", "potato"),
- e("🥕", "carrot"),
- e("🌽", "ear", "of", "corn"),
- e("🌶️", "hot", "pepper", "spicy"),
- e("🫑", "bell", "pepper"),
- e("🥒", "cucumber"),
- e("🥬", "leafy", "green"),
- e("🥦", "broccoli"),
- e("🧄", "garlic"),
- e("🧅", "onion"),
- e("🥜", "peanuts"),
- e("🫘", "beans"),
- e("🌰", "chestnut"),
- e("🫚", "ginger", "root"),
- e("🫛", "pea", "pod"),
- e("🍞", "bread", "toast"),
- e("🥐", "croissant"),
- e("🥖", "baguette", "bread"),
- e("🫓", "flatbread"),
- e("🥨", "pretzel"),
- e("🥯", "bagel"),
- e("🥞", "pancakes"),
- e("🧇", "waffle"),
- e("🧀", "cheese", "wedge"),
- e("🍖", "meat", "on", "bone"),
- e("🍗", "poultry", "leg", "meat", "chicken"),
- e("🥩", "cut", "of", "meat"),
- e("🥓", "bacon"),
- e("🍔", "hamburger", "burger"),
- e("🍟", "french", "fries"),
- e("🍕", "pizza"),
- e("🌭", "hot", "dog", "hotdog"),
- e("🥪", "sandwich"),
- e("🌮", "taco"),
- e("🌯", "burrito"),
- e("🫔", "tamale"),
- e("🥙", "stuffed", "flatbread"),
- e("🧆", "falafel"),
- e("🥚", "egg"),
- e("🍳", "cooking", "fried", "egg", "breakfast"),
- e("🥘", "shallow", "pan", "of", "food", "paella", "curry"),
- e("🍲", "pot", "of", "food", "stew"),
- e("🫕", "fondue"),
- e("🥣", "bowl", "with", "spoon"),
- e("🥗", "green", "salad"),
- e("🍿", "popcorn"),
- e("🧈", "butter"),
- e("🧂", "salt"),
- e("🥫", "canned", "food"),
- e("🍱", "bento", "box"),
- e("🍘", "rice", "cracker"),
- e("🍙", "rice", "ball"),
- e("🍚", "cooked", "rice"),
- e("🍛", "curry", "rice"),
- e("🍜", "steaming", "bowl", "ramen", "noodle"),
- e("🍝", "spaghetti", "pasta"),
- e("🍠", "roasted", "sweet", "potato"),
- e("🍢", "oden"),
- e("🍣", "sushi"),
- e("🍤", "fried", "shrimp", "tempura"),
- e("🍥", "fish", "cake", "with", "swirl"),
- e("🥮", "moon", "cake"),
- e("🍡", "dango"),
- e("🥟", "dumpling"),
- e("🥠", "fortune", "cookie"),
- e("🥡", "takeout", "box"),
- e("🦀", "crab"),
- e("🦞", "lobster"),
- e("🦐", "shrimp"),
- e("🦑", "squid"),
- e("🦪", "oyster"),
- e("🍦", "soft", "ice", "cream", "icecream"),
- e("🍧", "shaved", "ice"),
- e("🍨", "ice", "cream"),
- e("🍩", "doughnut"),
- e("🍪", "cookie"),
- e("🎂", "birthday", "cake", "party"),
- e("🍰", "shortcake", "cake", "dessert"),
- e("🧁", "cupcake"),
- e("🥧", "pie"),
- e("🍫", "chocolate", "bar"),
- e("🍬", "candy", "sweet"),
- e("🍭", "lollipop"),
- e("🍮", "custard"),
- e("🍯", "honey", "pot"),
- e("🍼", "baby", "bottle", "milk"),
- e("🥛", "glass", "of", "milk"),
- e("☕", "hot", "beverage", "coffee", "cafe", "espresso"),
- e("🫖", "teapot"),
- e("🍵", "teacup", "without", "handle", "tea", "green", "breakfast"),
- e("🍶", "sake"),
- e("🍾", "bottle", "with", "popping", "cork", "champagne", "bubbly", "celebration"),
- e("🍷", "wine", "glass"),
- e("🍸", "cocktail", "glass", "drink"),
- e("🍹", "tropical", "drink", "summer", "vacation"),
- e("🍺", "beer", "mug", "drink"),
- e("🍻", "clinking", "beer", "mugs", "beers", "drinks"),
- e("🥂", "clinking", "glasses", "cheers", "toast"),
- e("🥃", "tumbler", "glass", "whisky"),
- e("🫗", "pouring", "liquid"),
- e("🥤", "cup", "with", "straw"),
- e("🧋", "bubble", "tea"),
- e("🧃", "beverage", "box"),
- e("🧉", "mate"),
- e("🧊", "ice", "cube"),
- e("🥢", "chopsticks"),
- e("🍽️", "fork", "and", "knife", "with", "plate", "cutlery", "dining", "dinner"),
- e("🍴", "fork", "and", "knife", "cutlery"),
- e("🥄", "spoon"),
- e("🔪", "kitchen", "knife", "hocho", "cut", "chop"),
- e("🫙", "jar"),
- e("🏺", "amphora"),
- ),
- )
-
- private fun travel() = EmojiCategory(
- name = "Travel & Places",
- icon = "✈️",
- emojis =
- listOf(
- e("🌍", "globe", "showing", "europe", "africa", "earth", "world", "international"),
- e("🌎", "globe", "showing", "americas", "earth", "world", "international"),
- e("🌏", "globe", "showing", "asia", "australia", "earth", "world", "international"),
- e("🌐", "globe", "with", "meridians", "world", "global", "international"),
- e("🗺️", "world", "map", "travel"),
- e("🗾", "map", "of", "japan"),
- e("🧭", "compass"),
- e("🏔️", "snow", "capped", "mountain"),
- e("⛰️", "mountain"),
- e("🌋", "volcano"),
- e("🗻", "mount", "fuji"),
- e("🏕️", "camping"),
- e("🏖️", "beach", "with", "umbrella"),
- e("🏜️", "desert"),
- e("🏝️", "desert", "island"),
- e("🏞️", "national", "park"),
- e("🏟️", "stadium"),
- e("🏛️", "classical", "building"),
- e("🏗️", "building", "construction"),
- e("🧱", "brick", "bricks"),
- e("🪨", "rock"),
- e("🪵", "wood"),
- e("🛖", "hut"),
- e("🏘️", "houses"),
- e("🏚️", "derelict", "house"),
- e("🏠", "house"),
- e("🏡", "house", "with", "garden"),
- e("🏢", "office", "building"),
- e("🏣", "japanese", "post", "office"),
- e("🏤", "post", "office", "european"),
- e("🏥", "hospital"),
- e("🏦", "bank"),
- e("🏨", "hotel"),
- e("🏩", "love", "hotel"),
- e("🏪", "convenience", "store"),
- e("🏫", "school"),
- e("🏬", "department", "store"),
- e("🏭", "factory"),
- e("🏯", "japanese", "castle"),
- e("🏰", "castle", "european"),
- e("💒", "wedding", "marriage"),
- e("🗼", "tokyo", "tower"),
- e("🗽", "statue", "of", "liberty"),
- e("⛪", "church"),
- e("🕌", "mosque"),
- e("🛕", "hindu", "temple"),
- e("🕍", "synagogue"),
- e("⛩️", "shinto", "shrine"),
- e("🕋", "kaaba"),
- e("⛲", "fountain"),
- e("⛺", "tent", "camping"),
- e("🌁", "foggy", "karl"),
- e("🌃", "night", "with", "stars"),
- e("🏙️", "cityscape", "skyline"),
- e("🌄", "sunrise", "over", "mountains"),
- e("🌅", "sunrise"),
- e("🌆", "cityscape", "at", "dusk", "city", "sunset"),
- e("🌇", "sunset", "city", "sunrise"),
- e("🌉", "bridge", "at", "night"),
- e("♨️", "hot", "springs", "hotsprings"),
- e("🎠", "carousel", "horse"),
- e("🛝", "playground", "slide"),
- e("🎡", "ferris", "wheel"),
- e("🎢", "roller", "coaster"),
- e("💈", "barber", "pole"),
- e("🎪", "circus", "tent"),
- e("🚂", "locomotive", "steam", "train"),
- e("🚃", "railway", "car"),
- e("🚄", "high", "speed", "train", "bullettrain", "side"),
- e("🚅", "bullet", "train", "bullettrain", "front"),
- e("🚆", "train", "train2"),
- e("🚇", "metro"),
- e("🚈", "light", "rail"),
- e("🚉", "station"),
- e("🚊", "tram"),
- e("🚝", "monorail"),
- e("🚞", "mountain", "railway"),
- e("🚋", "tram", "car", "train"),
- e("🚌", "bus"),
- e("🚍", "oncoming", "bus"),
- e("🚎", "trolleybus"),
- e("🚐", "minibus"),
- e("🚑", "ambulance"),
- e("🚒", "fire", "engine"),
- e("🚓", "police", "car"),
- e("🚔", "oncoming", "police", "car"),
- e("🚕", "taxi"),
- e("🚖", "oncoming", "taxi"),
- e("🚗", "automobile", "car", "red"),
- e("🚘", "oncoming", "automobile"),
- e("🚙", "sport", "utility", "vehicle", "blue", "car"),
- e("🛻", "pickup", "truck"),
- e("🚚", "delivery", "truck"),
- e("🚛", "articulated", "lorry"),
- e("🚜", "tractor"),
- e("🏎️", "racing", "car"),
- e("🏍️", "motorcycle"),
- e("🛵", "motor", "scooter"),
- e("🦽", "manual", "wheelchair"),
- e("🦼", "motorized", "wheelchair"),
- e("🛺", "auto", "rickshaw"),
- e("🚲", "bicycle", "bike"),
- e("🛴", "kick", "scooter"),
- e("🛹", "skateboard"),
- e("🛼", "roller", "skate"),
- e("🚏", "bus", "stop", "busstop"),
- e("🛣️", "motorway"),
- e("🛤️", "railway", "track"),
- e("🛢️", "oil", "drum"),
- e("⛽", "fuel", "pump", "fuelpump"),
- e("🛞", "wheel"),
- e("🚨", "police", "car", "light", "rotating", "911", "emergency"),
- e("🚥", "horizontal", "traffic", "light"),
- e("🚦", "vertical", "traffic", "light", "semaphore"),
- e("🛑", "stop", "sign"),
- e("🚧", "construction", "wip"),
- e("⚓", "anchor", "ship"),
- e("🛟", "ring", "buoy", "lifepreserver"),
- e("⛵", "sailboat", "boat"),
- e("🛶", "canoe"),
- e("🚤", "speedboat", "ship"),
- e("🛳️", "passenger", "ship", "cruise"),
- e("⛴️", "ferry"),
- e("🛥️", "motor", "boat"),
- e("🚢", "ship"),
- e("✈️", "airplane", "flight"),
- e("🛩️", "small", "airplane", "flight"),
- e("🛫", "airplane", "departure", "flight"),
- e("🛬", "airplane", "arrival", "flight"),
- e("🪂", "parachute"),
- e("💺", "seat"),
- e("🚁", "helicopter"),
- e("🚟", "suspension", "railway"),
- e("🚠", "mountain", "cableway"),
- e("🚡", "aerial", "tramway"),
- e("🛰️", "satellite", "artificial", "orbit", "space"),
- e("🚀", "rocket", "ship", "launch"),
- e("🛸", "flying", "saucer", "ufo"),
- e("🛎️", "bellhop", "bell"),
- e("🧳", "luggage"),
- e("⌛", "hourglass", "done", "time"),
- e("⏳", "hourglass", "not", "done", "flowing", "sand", "time"),
- e("⌚", "watch", "time"),
- e("⏰", "alarm", "clock", "morning"),
- e("⏱️", "stopwatch"),
- e("⏲️", "timer", "clock"),
- e("🕰️", "mantelpiece", "clock"),
- e("🕛", "twelve", "clock12"),
- e("🕧", "twelve", "thirty", "clock1230"),
- e("🕐", "one", "clock1"),
- e("🕜", "one", "thirty", "clock130"),
- e("🕑", "two", "clock2"),
- e("🕝", "two", "thirty", "clock230"),
- e("🕒", "three", "clock3"),
- e("🕞", "three", "thirty", "clock330"),
- e("🕓", "four", "clock4"),
- e("🕟", "four", "thirty", "clock430"),
- e("🕔", "five", "clock5"),
- e("🕠", "five", "thirty", "clock530"),
- e("🕕", "six", "clock6"),
- e("🕡", "six", "thirty", "clock630"),
- e("🕖", "seven", "clock7"),
- e("🕢", "seven", "thirty", "clock730"),
- e("🕗", "eight", "clock8"),
- e("🕣", "eight", "thirty", "clock830"),
- e("🕘", "nine", "clock9"),
- e("🕤", "nine", "thirty", "clock930"),
- e("🕙", "ten", "clock10"),
- e("🕥", "ten", "thirty", "clock1030"),
- e("🕚", "eleven", "clock11"),
- e("🕦", "eleven", "thirty", "clock1130"),
- e("🌑", "new", "moon"),
- e("🌒", "waxing", "crescent", "moon"),
- e("🌓", "first", "quarter", "moon"),
- e("🌔", "waxing", "gibbous", "moon"),
- e("🌕", "full", "moon"),
- e("🌖", "waning", "gibbous", "moon"),
- e("🌗", "last", "quarter", "moon"),
- e("🌘", "waning", "crescent", "moon"),
- e("🌙", "crescent", "moon", "night"),
- e("🌚", "new", "moon", "face", "with"),
- e("🌛", "first", "quarter", "moon", "face", "with"),
- e("🌜", "last", "quarter", "moon", "face", "with"),
- e("🌡️", "thermometer"),
- e("☀️", "sun", "sunny", "weather"),
- e("🌝", "full", "moon", "face", "with"),
- e("🌞", "sun", "with", "face", "summer"),
- e("🪐", "ringed", "planet"),
- e("⭐", "star"),
- e("🌟", "glowing", "star", "star2"),
- e("🌠", "shooting", "star", "stars"),
- e("🌌", "milky", "way"),
- e("☁️", "cloud"),
- e("⛅", "sun", "behind", "cloud", "partly", "sunny", "weather"),
- e("⛈️", "cloud", "with", "lightning", "and", "rain"),
- e("🌤️", "sun", "behind", "small", "cloud"),
- e("🌥️", "sun", "behind", "large", "cloud"),
- e("🌦️", "sun", "behind", "rain", "cloud"),
- e("🌧️", "cloud", "with", "rain"),
- e("🌨️", "cloud", "with", "snow"),
- e("🌩️", "cloud", "with", "lightning"),
- e("🌪️", "tornado"),
- e("🌫️", "fog"),
- e("🌬️", "wind", "face"),
- e("🌀", "cyclone", "swirl"),
- e("🌈", "rainbow"),
- e("🌂", "closed", "umbrella", "weather", "rain"),
- e("☂️", "umbrella", "open"),
- e("☔", "umbrella", "with", "rain", "drops", "weather"),
- e("⛱️", "umbrella", "on", "ground", "parasol", "beachumbrella"),
- e("⚡", "high", "voltage", "zap", "lightning", "thunder"),
- e("❄️", "snowflake", "winter", "cold", "weather"),
- e("☃️", "snowman", "with", "snow", "winter", "christmas"),
- e("⛄", "snowman", "without", "snow", "winter"),
- e("☄️", "comet"),
- e("🔥", "fire", "burn"),
- e("💧", "droplet", "water"),
- e("🌊", "water", "wave", "ocean", "sea"),
- ),
- )
-
- private fun activities() = EmojiCategory(
- name = "Activities",
- icon = "⚽",
- emojis =
- listOf(
- e("🎃", "jack", "o", "lantern", "halloween"),
- e("🎄", "christmas", "tree"),
- e("🎆", "fireworks", "festival", "celebration"),
- e("🎇", "sparkler"),
- e("🧨", "firecracker"),
- e("✨", "sparkles", "shiny"),
- e("🎈", "balloon", "party", "birthday"),
- e("🎉", "party", "popper", "tada", "hooray"),
- e("🎊", "confetti", "ball"),
- e("🎋", "tanabata", "tree"),
- e("🎍", "pine", "decoration", "bamboo"),
- e("🎎", "japanese", "dolls"),
- e("🎏", "carp", "streamer", "flags"),
- e("🎐", "wind", "chime"),
- e("🎑", "moon", "viewing", "ceremony", "rice", "scene"),
- e("🧧", "red", "envelope"),
- e("🎀", "ribbon"),
- e("🎁", "wrapped", "gift", "present", "birthday", "christmas"),
- e("🎗️", "reminder", "ribbon"),
- e("🎟️", "admission", "tickets"),
- e("🎫", "ticket"),
- e("🎖️", "military", "medal"),
- e("🏆", "trophy", "award", "contest", "winner"),
- e("🏅", "sports", "medal", "gold", "winner"),
- e("🥇", "1st", "place", "medal", "gold"),
- e("🥈", "2nd", "place", "medal", "silver"),
- e("🥉", "3rd", "place", "medal", "bronze"),
- e("⚽", "soccer", "ball", "sports"),
- e("⚾", "baseball", "sports"),
- e("🥎", "softball"),
- e("🏀", "basketball", "sports"),
- e("🏐", "volleyball"),
- e("🏈", "american", "football", "sports"),
- e("🏉", "rugby", "football"),
- e("🎾", "tennis", "sports"),
- e("🥏", "flying", "disc"),
- e("🎳", "bowling"),
- e("🏏", "cricket", "game"),
- e("🏑", "field", "hockey"),
- e("🏒", "ice", "hockey"),
- e("🥍", "lacrosse"),
- e("🏓", "ping", "pong"),
- e("🏸", "badminton"),
- e("🥊", "boxing", "glove"),
- e("🥋", "martial", "arts", "uniform"),
- e("🥅", "goal", "net"),
- e("⛳", "flag", "in", "hole", "golf"),
- e("⛸️", "ice", "skate", "skating"),
- e("🎣", "fishing", "pole", "and", "fish"),
- e("🤿", "diving", "mask"),
- e("🎽", "running", "shirt", "with", "sash", "marathon"),
- e("🎿", "skis", "ski"),
- e("🛷", "sled"),
- e("🥌", "curling", "stone"),
- e("🎯", "bullseye", "dart", "target"),
- e("🪀", "yo"),
- e("🪁", "kite"),
- e("🔫", "water", "pistol", "gun", "shoot", "weapon"),
- e("🎱", "pool", "8", "ball", "8ball", "billiards"),
- e("🔮", "crystal", "ball", "fortune"),
- e("🪄", "magic", "wand"),
- e("🎮", "video", "game", "play", "controller", "console"),
- e("🕹️", "joystick"),
- e("🎰", "slot", "machine"),
- e("🎲", "game", "die", "dice", "gambling"),
- e("🧩", "puzzle", "piece", "jigsaw"),
- e("🧸", "teddy", "bear"),
- e("🪅", "piñata", "pinata"),
- e("🪩", "mirror", "ball", "disco", "party"),
- e("🪆", "nesting", "dolls"),
- e("♠️", "spade", "suit", "spades"),
- e("♥️", "heart", "suit", "hearts"),
- e("♦️", "diamond", "suit", "diamonds"),
- e("♣️", "club", "suit", "clubs"),
- e("♟️", "chess", "pawn"),
- e("🃏", "joker", "black"),
- e("🀄", "mahjong", "red", "dragon"),
- e("🎴", "flower", "playing", "cards"),
- e("🎭", "performing", "arts", "theater", "drama"),
- e("🖼️", "framed", "picture"),
- e("🎨", "artist", "palette", "art", "design", "paint"),
- e("🧵", "thread"),
- e("🪡", "sewing", "needle"),
- e("🧶", "yarn"),
- e("🪢", "knot"),
- ),
- )
-
- private fun objects() = EmojiCategory(
- name = "Objects",
- icon = "💡",
- emojis =
- listOf(
- e("👓", "glasses", "eyeglasses"),
- e("🕶️", "sunglasses", "dark"),
- e("🥽", "goggles"),
- e("🥼", "lab", "coat"),
- e("🦺", "safety", "vest"),
- e("👔", "necktie", "shirt", "formal"),
- e("👕", "t", "shirt", "tshirt"),
- e("👖", "jeans", "pants"),
- e("🧣", "scarf"),
- e("🧤", "gloves"),
- e("🧥", "coat"),
- e("🧦", "socks"),
- e("👗", "dress"),
- e("👘", "kimono"),
- e("🥻", "sari"),
- e("🩱", "one", "piece", "swimsuit"),
- e("🩲", "briefs", "swim", "brief"),
- e("🩳", "shorts"),
- e("👙", "bikini", "beach"),
- e("👚", "clothes", "womans"),
- e("🪭", "folding", "hand", "fan", "sensu"),
- e("👛", "purse"),
- e("👜", "handbag", "bag"),
- e("👝", "clutch", "bag", "pouch"),
- e("🛍️", "shopping", "bags"),
- e("🎒", "backpack", "school", "satchel"),
- e("🩴", "thong", "sandal"),
- e("👞", "shoe", "mans"),
- e("👟", "running", "shoe", "athletic", "sneaker", "sport"),
- e("🥾", "hiking", "boot"),
- e("🥿", "flat", "shoe"),
- e("👠", "high", "heeled", "shoe", "heel"),
- e("👡", "sandal", "shoe"),
- e("🩰", "ballet", "shoes"),
- e("👢", "boot"),
- e("🪮", "hair", "pick"),
- e("👑", "crown", "king", "queen", "royal"),
- e("👒", "hat", "womans"),
- e("🎩", "top", "hat", "tophat", "classy"),
- e("🎓", "graduation", "cap", "mortar", "board", "education", "college", "university"),
- e("🧢", "billed", "cap"),
- e("🪖", "military", "helmet"),
- e("⛑️", "rescue", "helmet", "worker"),
- e("📿", "prayer", "beads"),
- e("💄", "lipstick", "makeup"),
- e("💍", "ring", "wedding", "marriage", "engaged"),
- e("💎", "gem", "stone", "diamond"),
- e("🔇", "muted", "speaker", "mute", "sound", "volume"),
- e("🔈", "speaker", "low", "volume"),
- e("🔉", "speaker", "medium", "volume", "sound"),
- e("🔊", "speaker", "high", "volume", "loud", "sound"),
- e("📢", "loudspeaker", "announcement"),
- e("📣", "megaphone", "mega"),
- e("📯", "postal", "horn"),
- e("🔔", "bell", "sound", "notification"),
- e("🔕", "bell", "with", "slash", "no", "volume", "off"),
- e("🎼", "musical", "score"),
- e("🎵", "musical", "note"),
- e("🎶", "musical", "notes", "music"),
- e("🎙️", "studio", "microphone", "podcast"),
- e("🎚️", "level", "slider"),
- e("🎛️", "control", "knobs"),
- e("🎤", "microphone", "sing"),
- e("🎧", "headphone", "headphones", "music", "earphones"),
- e("📻", "radio", "podcast"),
- e("🎷", "saxophone"),
- e("🪗", "accordion"),
- e("🎸", "guitar", "rock"),
- e("🎹", "musical", "keyboard", "piano"),
- e("🎺", "trumpet"),
- e("🎻", "violin"),
- e("🪕", "banjo"),
- e("🥁", "drum"),
- e("🪘", "long", "drum"),
- e("🪇", "maracas", "shaker"),
- e("🪈", "flute", "recorder"),
- e("📱", "mobile", "phone", "iphone", "smartphone"),
- e("📲", "mobile", "phone", "with", "arrow", "calling", "call", "incoming"),
- e("☎️", "telephone", "phone"),
- e("📞", "telephone", "receiver", "phone", "call"),
- e("📟", "pager"),
- e("📠", "fax", "machine"),
- e("🔋", "battery", "power"),
- e("🪫", "low", "battery"),
- e("🔌", "electric", "plug"),
- e("💻", "laptop", "computer", "desktop", "screen"),
- e("🖥️", "desktop", "computer"),
- e("🖨️", "printer"),
- e("⌨️", "keyboard"),
- e("🖱️", "computer", "mouse"),
- e("🖲️", "trackball"),
- e("💽", "computer", "disk", "minidisc"),
- e("💾", "floppy", "disk", "save"),
- e("💿", "optical", "disk", "cd"),
- e("📀", "dvd"),
- e("🧮", "abacus"),
- e("🎥", "movie", "camera", "film", "video"),
- e("🎞️", "film", "frames", "strip"),
- e("📽️", "film", "projector"),
- e("🎬", "clapper", "board", "film"),
- e("📺", "television", "tv"),
- e("📷", "camera", "photo"),
- e("📸", "camera", "with", "flash", "photo"),
- e("📹", "video", "camera"),
- e("📼", "videocassette", "vhs"),
- e("🔍", "magnifying", "glass", "tilted", "left", "mag", "search", "zoom"),
- e("🔎", "magnifying", "glass", "tilted", "right", "mag"),
- e("🕯️", "candle"),
- e("💡", "light", "bulb", "idea"),
- e("🔦", "flashlight"),
- e("🏮", "red", "paper", "lantern", "izakaya"),
- e("🪔", "diya", "lamp"),
- e("📔", "notebook", "with", "decorative", "cover"),
- e("📕", "closed", "book"),
- e("📖", "open", "book"),
- e("📗", "green", "book"),
- e("📘", "blue", "book"),
- e("📙", "orange", "book"),
- e("📚", "books", "library"),
- e("📓", "notebook"),
- e("📒", "ledger"),
- e("📃", "page", "with", "curl"),
- e("📜", "scroll", "document"),
- e("📄", "page", "facing", "up", "document"),
- e("📰", "newspaper", "press"),
- e("🗞️", "rolled", "up", "newspaper", "roll", "press"),
- e("📑", "bookmark", "tabs"),
- e("🔖", "bookmark"),
- e("🏷️", "label", "tag"),
- e("💰", "money", "bag", "moneybag", "dollar", "cream"),
- e("🪙", "coin"),
- e("💴", "yen", "banknote"),
- e("💵", "dollar", "banknote", "money"),
- e("💶", "euro", "banknote"),
- e("💷", "pound", "banknote"),
- e("💸", "money", "with", "wings", "dollar"),
- e("💳", "credit", "card", "subscription"),
- e("🧾", "receipt"),
- e("💹", "chart", "increasing", "with", "yen"),
- e("✉️", "envelope", "letter", "email"),
- e("📧", "e", "mail", "email"),
- e("📨", "incoming", "envelope"),
- e("📩", "envelope", "with", "arrow"),
- e("📤", "outbox", "tray"),
- e("📥", "inbox", "tray"),
- e("📦", "package", "shipping"),
- e("📫", "closed", "mailbox", "with", "raised", "flag"),
- e("📪", "closed", "mailbox", "with", "lowered", "flag"),
- e("📬", "open", "mailbox", "with", "raised", "flag", "mail"),
- e("📭", "open", "mailbox", "with", "lowered", "flag", "no", "mail"),
- e("📮", "postbox"),
- e("🗳️", "ballot", "box", "with"),
- e("✏️", "pencil", "pencil2"),
- e("✒️", "black", "nib"),
- e("🖋️", "fountain", "pen"),
- e("🖊️", "pen"),
- e("🖌️", "paintbrush"),
- e("🖍️", "crayon"),
- e("📝", "memo", "pencil", "document", "note"),
- e("💼", "briefcase", "business"),
- e("📁", "file", "folder", "directory"),
- e("📂", "open", "file", "folder"),
- e("🗂️", "card", "index", "dividers"),
- e("📅", "calendar", "date", "schedule"),
- e("📆", "tear", "off", "calendar", "schedule"),
- e("🗒️", "spiral", "notepad"),
- e("🗓️", "spiral", "calendar"),
- e("📇", "card", "index"),
- e("📈", "chart", "increasing", "with", "upwards", "trend", "graph", "metrics"),
- e("📉", "chart", "decreasing", "with", "downwards", "trend", "graph", "metrics"),
- e("📊", "bar", "chart", "stats", "metrics"),
- e("📋", "clipboard"),
- e("📌", "pushpin", "location"),
- e("📍", "round", "pushpin", "location"),
- e("📎", "paperclip"),
- e("🖇️", "linked", "paperclips"),
- e("📏", "straight", "ruler"),
- e("📐", "triangular", "ruler"),
- e("✂️", "scissors", "cut"),
- e("🗃️", "card", "file", "box"),
- e("🗄️", "file", "cabinet"),
- e("🗑️", "wastebasket", "trash"),
- e("🔒", "locked", "lock", "security", "private"),
- e("🔓", "unlocked", "unlock", "security"),
- e("🔏", "locked", "with", "pen", "lock", "ink"),
- e("🔐", "locked", "with", "key", "closed", "lock", "security"),
- e("🔑", "key", "lock", "password"),
- e("🗝️", "old", "key"),
- e("🔨", "hammer", "tool"),
- e("🪓", "axe"),
- e("⛏️", "pick"),
- e("⚒️", "hammer", "and", "pick"),
- e("🛠️", "hammer", "and", "wrench"),
- e("🗡️", "dagger"),
- e("⚔️", "crossed", "swords"),
- e("💣", "bomb", "boom"),
- e("🪃", "boomerang"),
- e("🏹", "bow", "and", "arrow", "archery"),
- e("🛡️", "shield"),
- e("🪚", "carpentry", "saw"),
- e("🔧", "wrench", "tool"),
- e("🪛", "screwdriver"),
- e("🔩", "nut", "and", "bolt"),
- e("⚙️", "gear"),
- e("🗜️", "clamp"),
- e("⚖️", "balance", "scale"),
- e("🦯", "white", "cane", "probing"),
- e("🔗", "link"),
- e("⛓️", "chains"),
- e("🪝", "hook"),
- e("🧰", "toolbox"),
- e("🧲", "magnet"),
- e("🪜", "ladder"),
- e("⚗️", "alembic"),
- e("🧪", "test", "tube"),
- e("🧫", "petri", "dish"),
- e("🧬", "dna"),
- e("🔬", "microscope", "science", "laboratory", "investigate"),
- e("🔭", "telescope"),
- e("📡", "satellite", "antenna", "signal"),
- e("💉", "syringe", "health", "hospital", "needle"),
- e("🩸", "drop", "of", "blood"),
- e("💊", "pill", "health", "medicine"),
- e("🩹", "adhesive", "bandage"),
- e("🩼", "crutch"),
- e("🩺", "stethoscope"),
- e("🩻", "x", "ray"),
- e("🚪", "door"),
- e("🛗", "elevator"),
- e("🪞", "mirror"),
- e("🪟", "window"),
- e("🛏️", "bed"),
- e("🛋️", "couch", "and", "lamp"),
- e("🪑", "chair"),
- e("🚽", "toilet", "wc"),
- e("🪠", "plunger"),
- e("🚿", "shower", "bath"),
- e("🛁", "bathtub"),
- e("🪤", "mouse", "trap"),
- e("🪒", "razor"),
- e("🧴", "lotion", "bottle"),
- e("🧷", "safety", "pin"),
- e("🧹", "broom"),
- e("🧺", "basket"),
- e("🧻", "roll", "of", "paper", "toilet"),
- e("🪣", "bucket"),
- e("🧼", "soap"),
- e("🫧", "bubbles"),
- e("🪥", "toothbrush"),
- e("🧽", "sponge"),
- e("🧯", "fire", "extinguisher"),
- e("🛒", "shopping", "cart"),
- e("🚬", "cigarette", "smoking"),
- e("⚰️", "coffin", "funeral"),
- e("🪦", "headstone"),
- e("⚱️", "funeral", "urn"),
- e("🧿", "nazar", "amulet"),
- e("🪬", "hamsa"),
- e("🗿", "moai", "moyai", "stone"),
- e("🪧", "placard"),
- e("🪪", "identification", "card"),
- ),
- )
-
- private fun symbols() = EmojiCategory(
- name = "Symbols",
- icon = "🔣",
- emojis =
- listOf(
- e("🏧", "atm", "sign"),
- e("🚮", "litter", "in", "bin", "sign", "put", "its", "place"),
- e("🚰", "potable", "water"),
- e("♿", "wheelchair", "symbol", "accessibility"),
- e("🚹", "room", "mens"),
- e("🚺", "room", "womens"),
- e("🚻", "restroom", "toilet"),
- e("🚼", "baby", "symbol"),
- e("🚾", "water", "closet", "wc", "toilet", "restroom"),
- e("🛂", "passport", "control"),
- e("🛃", "customs"),
- e("🛄", "baggage", "claim", "airport"),
- e("🛅", "left", "luggage"),
- e("⚠️", "warning", "wip"),
- e("🚸", "children", "crossing"),
- e("⛔", "no", "entry", "limit"),
- e("🚫", "prohibited", "no", "entry", "sign", "block", "forbidden"),
- e("🚳", "no", "bicycles"),
- e("🚭", "no", "smoking"),
- e("🚯", "no", "littering", "do", "not", "litter"),
- e("🚱", "non", "potable", "water"),
- e("🚷", "no", "pedestrians"),
- e("📵", "no", "mobile", "phones"),
- e("🔞", "no", "one", "under", "eighteen", "underage"),
- e("☢️", "radioactive"),
- e("☣️", "biohazard"),
- e("⬆️", "up", "arrow"),
- e("↗️", "up", "right", "arrow", "upper"),
- e("➡️", "right", "arrow"),
- e("↘️", "down", "right", "arrow", "lower"),
- e("⬇️", "down", "arrow"),
- e("↙️", "down", "left", "arrow", "lower"),
- e("⬅️", "left", "arrow"),
- e("↖️", "up", "left", "arrow", "upper"),
- e("↕️", "up", "down", "arrow"),
- e("↔️", "left", "right", "arrow"),
- e("↩️", "right", "arrow", "curving", "left", "leftwards", "with", "hook", "return"),
- e("↪️", "left", "arrow", "curving", "right", "hook"),
- e("⤴️", "right", "arrow", "curving", "up", "heading"),
- e("⤵️", "right", "arrow", "curving", "down", "heading"),
- e("🔃", "clockwise", "vertical", "arrows"),
- e("🔄", "counterclockwise", "arrows", "button", "sync"),
- e("🔙", "back", "arrow"),
- e("🔚", "end", "arrow"),
- e("🔛", "arrow", "on"),
- e("🔜", "soon", "arrow"),
- e("🔝", "top", "arrow"),
- e("🛐", "place", "of", "worship"),
- e("⚛️", "atom", "symbol"),
- e("🕉️", "om"),
- e("✡️", "star", "of", "david"),
- e("☸️", "wheel", "of", "dharma"),
- e("☯️", "yin", "yang"),
- e("✝️", "latin", "cross"),
- e("☦️", "orthodox", "cross"),
- e("☪️", "star", "and", "crescent"),
- e("☮️", "peace", "symbol"),
- e("🕎", "menorah"),
- e("🔯", "dotted", "six", "pointed", "star"),
- e("🪯", "khanda"),
- e("♈", "aries"),
- e("♉", "taurus"),
- e("♊", "gemini"),
- e("♋", "cancer"),
- e("♌", "leo"),
- e("♍", "virgo"),
- e("♎", "libra"),
- e("♏", "scorpio", "scorpius"),
- e("♐", "sagittarius"),
- e("♑", "capricorn"),
- e("♒", "aquarius"),
- e("♓", "pisces"),
- e("⛎", "ophiuchus"),
- e("🔀", "shuffle", "tracks", "button", "twisted", "rightwards", "arrows"),
- e("🔁", "repeat", "button", "loop"),
- e("🔂", "repeat", "single", "button", "one"),
- e("▶️", "play", "button", "arrow", "forward"),
- e("⏩", "fast", "forward", "button"),
- e("⏭️", "next", "track", "button"),
- e("⏯️", "play", "or", "pause", "button"),
- e("◀️", "reverse", "button", "arrow", "backward"),
- e("⏪", "fast", "reverse", "button", "rewind"),
- e("⏮️", "last", "track", "button", "previous"),
- e("🔼", "upwards", "button", "arrow", "up", "small"),
- e("⏫", "fast", "up", "button", "arrow", "double"),
- e("🔽", "downwards", "button", "arrow", "down", "small"),
- e("⏬", "fast", "down", "button", "arrow", "double"),
- e("⏸️", "pause", "button"),
- e("⏹️", "stop", "button"),
- e("⏺️", "record", "button"),
- e("⏏️", "eject", "button"),
- e("🎦", "cinema", "film", "movie"),
- e("🔅", "dim", "button", "low", "brightness"),
- e("🔆", "bright", "button", "high", "brightness"),
- e("📶", "antenna", "bars", "signal", "strength", "wifi"),
- e("🛜", "wireless", "wifi"),
- e("📳", "vibration", "mode"),
- e("📴", "mobile", "phone", "off", "mute"),
- e("♀️", "female", "sign"),
- e("♂️", "male", "sign"),
- e("⚧️", "transgender", "symbol"),
- e("✖️", "multiply", "heavy", "multiplication", "x"),
- e("➕", "plus", "heavy", "sign"),
- e("➖", "minus", "heavy", "sign"),
- e("➗", "divide", "heavy", "division", "sign"),
- e("🟰", "heavy", "equals", "sign"),
- e("♾️", "infinity"),
- e("‼️", "double", "exclamation", "mark", "bangbang"),
- e("⁉️", "exclamation", "question", "mark", "interrobang"),
- e("❓", "red", "question", "mark", "confused"),
- e("❔", "white", "question", "mark", "grey"),
- e("❕", "white", "exclamation", "mark", "grey"),
- e("❗", "red", "exclamation", "mark", "heavy", "bang"),
- e("〰️", "wavy", "dash"),
- e("💱", "currency", "exchange"),
- e("💲", "heavy", "dollar", "sign"),
- e("⚕️", "medical", "symbol"),
- e("♻️", "recycling", "symbol", "recycle", "environment", "green"),
- e("⚜️", "fleur", "de", "lis"),
- e("🔱", "trident", "emblem"),
- e("📛", "name", "badge"),
- e("🔰", "japanese", "symbol", "for", "beginner"),
- e("⭕", "hollow", "red", "circle", "o"),
- e("✅", "check", "mark", "button", "white"),
- e("☑️", "check", "box", "with", "ballot"),
- e("✔️", "check", "mark", "heavy"),
- e("❌", "cross", "mark", "x"),
- e("❎", "cross", "mark", "button", "negative", "squared"),
- e("➰", "curly", "loop"),
- e("➿", "double", "curly", "loop"),
- e("〽️", "part", "alternation", "mark"),
- e("✳️", "eight", "spoked", "asterisk"),
- e("✴️", "eight", "pointed", "star", "black"),
- e("❇️", "sparkle"),
- e("©️", "copyright"),
- e("®️", "registered"),
- e("™️", "trade", "mark", "tm", "trademark"),
- e("#️⃣", "hash", "number"),
- e("*️⃣", "asterisk"),
- e("0️⃣", "0", "zero"),
- e("1️⃣", "1", "one"),
- e("2️⃣", "2", "two"),
- e("3️⃣", "3", "three"),
- e("4️⃣", "4", "four"),
- e("5️⃣", "5", "five"),
- e("6️⃣", "6", "six"),
- e("7️⃣", "7", "seven"),
- e("8️⃣", "8", "eight"),
- e("9️⃣", "9", "nine"),
- e("🔟", "10", "keycap", "ten"),
- e("🔠", "input", "latin", "uppercase", "capital", "abcd", "letters"),
- e("🔡", "input", "latin", "lowercase", "abcd"),
- e("🔢", "input", "numbers", "1234"),
- e("🔣", "input", "symbols"),
- e("🔤", "input", "latin", "letters", "abc", "alphabet"),
- e("🅰️", "a", "button"),
- e("🆎", "ab", "button"),
- e("🅱️", "b", "button"),
- e("🆑", "cl", "button"),
- e("🆒", "cool", "button"),
- e("🆓", "free", "button"),
- e("ℹ️", "information", "source"),
- e("🆔", "id", "button"),
- e("Ⓜ️", "circled", "m"),
- e("🆕", "new", "button", "fresh"),
- e("🆖", "ng", "button"),
- e("🅾️", "o", "button", "o2"),
- e("🆗", "ok", "button", "yes"),
- e("🅿️", "p", "button", "parking"),
- e("🆘", "sos", "button", "help", "emergency"),
- e("🆙", "button", "up"),
- e("🆚", "vs", "button"),
- e("🈁", "japanese", "button", "koko"),
- e("🈂️", "japanese", "button", "sa"),
- e("🈷️", "japanese", "button", "u6708"),
- e("🈶", "japanese", "free", "of", "button", "u6709"),
- e("🈯", "japanese", "button", "u6307"),
- e("🉐", "japanese", "button", "ideograph", "advantage"),
- e("🈹", "japanese", "button", "u5272"),
- e("🈚", "japanese", "of", "button", "u7121"),
- e("🈲", "japanese", "button", "u7981"),
- e("🉑", "japanese", "button", "accept"),
- e("🈸", "japanese", "button", "u7533"),
- e("🈴", "japanese", "button", "u5408"),
- e("🈳", "japanese", "button", "u7a7a"),
- e("㊗️", "japanese", "button", "congratulations"),
- e("㊙️", "japanese", "button", "secret"),
- e("🈺", "japanese", "for", "button", "u55b6"),
- e("🈵", "japanese", "button", "u6e80"),
- e("🔴", "red", "circle"),
- e("🟠", "orange", "circle"),
- e("🟡", "yellow", "circle"),
- e("🟢", "green", "circle"),
- e("🔵", "blue", "circle", "large"),
- e("🟣", "purple", "circle"),
- e("🟤", "brown", "circle"),
- e("⚫", "black", "circle"),
- e("⚪", "white", "circle"),
- e("🟥", "red", "square"),
- e("🟧", "orange", "square"),
- e("🟨", "yellow", "square"),
- e("🟩", "green", "square"),
- e("🟦", "blue", "square"),
- e("🟪", "purple", "square"),
- e("🟫", "brown", "square"),
- e("⬛", "black", "large", "square"),
- e("⬜", "white", "large", "square"),
- e("◼️", "black", "medium", "square"),
- e("◻️", "white", "medium", "square"),
- e("◾", "black", "medium", "small", "square"),
- e("◽", "white", "medium", "small", "square"),
- e("▪️", "black", "small", "square"),
- e("▫️", "white", "small", "square"),
- e("🔶", "large", "orange", "diamond"),
- e("🔷", "large", "blue", "diamond"),
- e("🔸", "small", "orange", "diamond"),
- e("🔹", "small", "blue", "diamond"),
- e("🔺", "red", "triangle", "pointed", "up", "small"),
- e("🔻", "red", "triangle", "pointed", "down", "small"),
- e("💠", "diamond", "with", "a", "dot", "shape", "inside"),
- e("🔘", "radio", "button"),
- e("🔳", "white", "square", "button"),
- e("🔲", "black", "square", "button"),
- ),
- )
-
- private fun flags() = EmojiCategory(
- name = "Flags",
- icon = "🏁",
- emojis =
- listOf(
- e("🏁", "chequered", "flag", "checkered", "milestone", "finish"),
- e("🚩", "triangular", "flag", "on", "post"),
- e("🎌", "crossed", "flags"),
- e("🏴", "black", "flag"),
- e("🏳️", "white", "flag"),
- e("🏳️‍🌈", "rainbow", "flag", "pride"),
- e("🏳️‍⚧️", "transgender", "flag"),
- e("🏴‍☠️", "pirate", "flag"),
- e("🇦🇨", "ascension", "island"),
- e("🇦🇩", "andorra"),
- e("🇦🇪", "united", "arab", "emirates"),
- e("🇦🇫", "afghanistan"),
- e("🇦🇬", "antigua", "barbuda"),
- e("🇦🇮", "anguilla"),
- e("🇦🇱", "albania"),
- e("🇦🇲", "armenia"),
- e("🇦🇴", "angola"),
- e("🇦🇶", "antarctica"),
- e("🇦🇷", "argentina"),
- e("🇦🇸", "american", "samoa"),
- e("🇦🇹", "austria"),
- e("🇦🇺", "australia"),
- e("🇦🇼", "aruba"),
- e("🇦🇽", "åland", "islands", "aland"),
- e("🇦🇿", "azerbaijan"),
- e("🇧🇦", "bosnia", "herzegovina"),
- e("🇧🇧", "barbados"),
- e("🇧🇩", "bangladesh"),
- e("🇧🇪", "belgium"),
- e("🇧🇫", "burkina", "faso"),
- e("🇧🇬", "bulgaria"),
- e("🇧🇭", "bahrain"),
- e("🇧🇮", "burundi"),
- e("🇧🇯", "benin"),
- e("🇧🇱", "barthélemy", "st", "barthelemy"),
- e("🇧🇲", "bermuda"),
- e("🇧🇳", "brunei"),
- e("🇧🇴", "bolivia"),
- e("🇧🇶", "caribbean", "netherlands"),
- e("🇧🇷", "brazil"),
- e("🇧🇸", "bahamas"),
- e("🇧🇹", "bhutan"),
- e("🇧🇻", "bouvet", "island"),
- e("🇧🇼", "botswana"),
- e("🇧🇾", "belarus"),
- e("🇧🇿", "belize"),
- e("🇨🇦", "canada"),
- e("🇨🇨", "cocos", "islands", "keeling"),
- e("🇨🇩", "congo", "kinshasa"),
- e("🇨🇫", "central", "african", "republic"),
- e("🇨🇬", "congo", "brazzaville"),
- e("🇨🇭", "switzerland"),
- e("🇨🇮", "côte", "cote", "divoire", "ivory"),
- e("🇨🇰", "cook", "islands"),
- e("🇨🇱", "chile"),
- e("🇨🇲", "cameroon"),
- e("🇨🇳", "china", "cn"),
- e("🇨🇴", "colombia"),
- e("🇨🇵", "clipperton", "island"),
- e("🇨🇷", "costa", "rica"),
- e("🇨🇺", "cuba"),
- e("🇨🇻", "cape", "verde"),
- e("🇨🇼", "curaçao", "curacao"),
- e("🇨🇽", "christmas", "island"),
- e("🇨🇾", "cyprus"),
- e("🇨🇿", "czechia", "czech", "republic"),
- e("🇩🇪", "germany", "de", "flag"),
- e("🇩🇬", "diego", "garcia"),
- e("🇩🇯", "djibouti"),
- e("🇩🇰", "denmark"),
- e("🇩🇲", "dominica"),
- e("🇩🇴", "dominican", "republic"),
- e("🇩🇿", "algeria"),
- e("🇪🇦", "ceuta", "melilla"),
- e("🇪🇨", "ecuador"),
- e("🇪🇪", "estonia"),
- e("🇪🇬", "egypt"),
- e("🇪🇭", "western", "sahara"),
- e("🇪🇷", "eritrea"),
- e("🇪🇸", "spain", "es"),
- e("🇪🇹", "ethiopia"),
- e("🇪🇺", "european", "union", "eu"),
- e("🇫🇮", "finland"),
- e("🇫🇯", "fiji"),
- e("🇫🇰", "falkland", "islands"),
- e("🇫🇲", "micronesia"),
- e("🇫🇴", "faroe", "islands"),
- e("🇫🇷", "france", "fr", "french"),
- e("🇬🇦", "gabon"),
- e("🇬🇧", "united", "kingdom", "gb", "uk", "flag", "british"),
- e("🇬🇩", "grenada"),
- e("🇬🇪", "georgia"),
- e("🇬🇫", "french", "guiana"),
- e("🇬🇬", "guernsey"),
- e("🇬🇭", "ghana"),
- e("🇬🇮", "gibraltar"),
- e("🇬🇱", "greenland"),
- e("🇬🇲", "gambia"),
- e("🇬🇳", "guinea"),
- e("🇬🇵", "guadeloupe"),
- e("🇬🇶", "equatorial", "guinea"),
- e("🇬🇷", "greece"),
- e("🇬🇸", "south", "georgia", "sandwich", "islands"),
- e("🇬🇹", "guatemala"),
- e("🇬🇺", "guam"),
- e("🇬🇼", "guinea", "bissau"),
- e("🇬🇾", "guyana"),
- e("🇭🇰", "hong", "kong", "sar", "china"),
- e("🇭🇲", "heard", "mcdonald", "islands"),
- e("🇭🇳", "honduras"),
- e("🇭🇷", "croatia"),
- e("🇭🇹", "haiti"),
- e("🇭🇺", "hungary"),
- e("🇮🇨", "canary", "islands"),
- e("🇮🇩", "indonesia"),
- e("🇮🇪", "ireland"),
- e("🇮🇱", "israel"),
- e("🇮🇲", "isle", "of", "man"),
- e("🇮🇳", "india"),
- e("🇮🇴", "british", "indian", "ocean", "territory"),
- e("🇮🇶", "iraq"),
- e("🇮🇷", "iran"),
- e("🇮🇸", "iceland"),
- e("🇮🇹", "italy", "it"),
- e("🇯🇪", "jersey"),
- e("🇯🇲", "jamaica"),
- e("🇯🇴", "jordan"),
- e("🇯🇵", "japan", "jp"),
- e("🇰🇪", "kenya"),
- e("🇰🇬", "kyrgyzstan"),
- e("🇰🇭", "cambodia"),
- e("🇰🇮", "kiribati"),
- e("🇰🇲", "comoros"),
- e("🇰🇳", "kitts", "nevis", "st"),
- e("🇰🇵", "north", "korea"),
- e("🇰🇷", "south", "korea", "kr"),
- e("🇰🇼", "kuwait"),
- e("🇰🇾", "cayman", "islands"),
- e("🇰🇿", "kazakhstan"),
- e("🇱🇦", "laos"),
- e("🇱🇧", "lebanon"),
- e("🇱🇨", "lucia", "st"),
- e("🇱🇮", "liechtenstein"),
- e("🇱🇰", "sri", "lanka"),
- e("🇱🇷", "liberia"),
- e("🇱🇸", "lesotho"),
- e("🇱🇹", "lithuania"),
- e("🇱🇺", "luxembourg"),
- e("🇱🇻", "latvia"),
- e("🇱🇾", "libya"),
- e("🇲🇦", "morocco"),
- e("🇲🇨", "monaco"),
- e("🇲🇩", "moldova"),
- e("🇲🇪", "montenegro"),
- e("🇲🇫", "martin", "st"),
- e("🇲🇬", "madagascar"),
- e("🇲🇭", "marshall", "islands"),
- e("🇲🇰", "north", "macedonia"),
- e("🇲🇱", "mali"),
- e("🇲🇲", "myanmar", "burma"),
- e("🇲🇳", "mongolia"),
- e("🇲🇴", "macao", "sar", "china", "macau"),
- e("🇲🇵", "northern", "mariana", "islands"),
- e("🇲🇶", "martinique"),
- e("🇲🇷", "mauritania"),
- e("🇲🇸", "montserrat"),
- e("🇲🇹", "malta"),
- e("🇲🇺", "mauritius"),
- e("🇲🇻", "maldives"),
- e("🇲🇼", "malawi"),
- e("🇲🇽", "mexico"),
- e("🇲🇾", "malaysia"),
- e("🇲🇿", "mozambique"),
- e("🇳🇦", "namibia"),
- e("🇳🇨", "new", "caledonia"),
- e("🇳🇪", "niger"),
- e("🇳🇫", "norfolk", "island"),
- e("🇳🇬", "nigeria"),
- e("🇳🇮", "nicaragua"),
- e("🇳🇱", "netherlands"),
- e("🇳🇴", "norway"),
- e("🇳🇵", "nepal"),
- e("🇳🇷", "nauru"),
- e("🇳🇺", "niue"),
- e("🇳🇿", "new", "zealand"),
- e("🇴🇲", "oman"),
- e("🇵🇦", "panama"),
- e("🇵🇪", "peru"),
- e("🇵🇫", "french", "polynesia"),
- e("🇵🇬", "papua", "new", "guinea"),
- e("🇵🇭", "philippines"),
- e("🇵🇰", "pakistan"),
- e("🇵🇱", "poland"),
- e("🇵🇲", "pierre", "miquelon", "st"),
- e("🇵🇳", "pitcairn", "islands"),
- e("🇵🇷", "puerto", "rico"),
- e("🇵🇸", "palestinian", "territories"),
- e("🇵🇹", "portugal"),
- e("🇵🇼", "palau"),
- e("🇵🇾", "paraguay"),
- e("🇶🇦", "qatar"),
- e("🇷🇪", "réunion", "reunion"),
- e("🇷🇴", "romania"),
- e("🇷🇸", "serbia"),
- e("🇷🇺", "russia", "ru"),
- e("🇷🇼", "rwanda"),
- e("🇸🇦", "saudi", "arabia"),
- e("🇸🇧", "solomon", "islands"),
- e("🇸🇨", "seychelles"),
- e("🇸🇩", "sudan"),
- e("🇸🇪", "sweden"),
- e("🇸🇬", "singapore"),
- e("🇸🇭", "helena", "st"),
- e("🇸🇮", "slovenia"),
- e("🇸🇯", "svalbard", "jan", "mayen"),
- e("🇸🇰", "slovakia"),
- e("🇸🇱", "sierra", "leone"),
- e("🇸🇲", "san", "marino"),
- e("🇸🇳", "senegal"),
- e("🇸🇴", "somalia"),
- e("🇸🇷", "suriname"),
- e("🇸🇸", "south", "sudan"),
- e("🇸🇹", "são", "tomé", "príncipe", "sao", "tome", "principe"),
- e("🇸🇻", "el", "salvador"),
- e("🇸🇽", "sint", "maarten"),
- e("🇸🇾", "syria"),
- e("🇸🇿", "eswatini", "swaziland"),
- e("🇹🇦", "tristan", "da", "cunha"),
- e("🇹🇨", "turks", "caicos", "islands"),
- e("🇹🇩", "chad"),
- e("🇹🇫", "french", "southern", "territories"),
- e("🇹🇬", "togo"),
- e("🇹🇭", "thailand"),
- e("🇹🇯", "tajikistan"),
- e("🇹🇰", "tokelau"),
- e("🇹🇱", "timor", "leste"),
- e("🇹🇲", "turkmenistan"),
- e("🇹🇳", "tunisia"),
- e("🇹🇴", "tonga"),
- e("🇹🇷", "turkey", "tr"),
- e("🇹🇹", "trinidad", "tobago"),
- e("🇹🇻", "tuvalu"),
- e("🇹🇼", "taiwan"),
- e("🇹🇿", "tanzania"),
- e("🇺🇦", "ukraine"),
- e("🇺🇬", "uganda"),
- e("🇺🇲", "outlying", "islands", "us"),
- e("🇺🇳", "united", "nations"),
- e("🇺🇸", "united", "states", "us", "flag", "america"),
- e("🇺🇾", "uruguay"),
- e("🇺🇿", "uzbekistan"),
- e("🇻🇦", "vatican", "city"),
- e("🇻🇨", "vincent", "grenadines", "st"),
- e("🇻🇪", "venezuela"),
- e("🇻🇬", "british", "virgin", "islands"),
- e("🇻🇮", "virgin", "islands", "us"),
- e("🇻🇳", "vietnam"),
- e("🇻🇺", "vanuatu"),
- e("🇼🇫", "wallis", "futuna"),
- e("🇼🇸", "samoa"),
- e("🇽🇰", "kosovo"),
- e("🇾🇪", "yemen"),
- e("🇾🇹", "mayotte"),
- e("🇿🇦", "south", "africa"),
- e("🇿🇲", "zambia"),
- e("🇿🇼", "zimbabwe"),
- e("🏴󠁧󠁢󠁥󠁮󠁧󠁿", "england"),
- e("🏴󠁧󠁢󠁳󠁣󠁴󠁿", "scotland"),
- e("🏴󠁧󠁢󠁷󠁬󠁳󠁿", "wales"),
- ),
- )
-}

diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/emoji/EmojiPickerDialog.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/emoji/EmojiPickerDialog.kt
index ee9bbdbd04..cbf5e9074c 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/emoji/EmojiPickerDialog.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/emoji/EmojiPickerDialog.kt
@@ -15,6 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@file:Suppress("TooManyFunctions")
+@file:OptIn(ExperimentalMaterial3Api::class)
package org.meshtastic.core.ui.emoji
@@ -31,7 +32,6 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
@@ -43,17 +43,22 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.PrimaryScrollableTabRow
import androidx.compose.material3.Surface
import androidx.compose.material3.Tab
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
+import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -67,18 +72,20 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Popup
+import kotlinx.coroutines.delay
import org.jetbrains.compose.resources.stringResource
import org.koin.compose.viewmodel.koinViewModel
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.clear
import org.meshtastic.core.resources.search_emoji
-import org.meshtastic.core.ui.component.BottomSheetDialog
import org.meshtastic.core.ui.icon.Close
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.Search
+import org.meshtastic.core.ui.theme.AppTheme
// ── Constants ──────────────────────────────────────────────────────────────────
@@ -89,6 +96,7 @@ private const val RECENTS_HEADER_KEY = "header_recents"
private const val RECENTS_KEY_PREFIX = "recent_"
private const val MAX_RECENTS = 30
private const val DEFAULT_QUICK_REACTION_COUNT = 6
+private const val SEARCH_DEBOUNCE_MS = 150L
/** Default quick-reaction emoji used when the user has no recents. */
private val DEFAULT_QUICK_REACTIONS = listOf("👍", "❤️", "😂", "😮", "😢", "🙏")
@@ -116,26 +124,53 @@ fun EmojiPickerDialog(
onConfirm: (String) -> Unit,
) {
val viewModel: EmojiPickerViewModel = koinViewModel()
+ val isLoaded by viewModel.isLoaded.collectAsState()
var searchQuery by rememberSaveable { mutableStateOf("") }
+ var debouncedQuery by remember { mutableStateOf("") }
var selectedCategoryIndex by rememberSaveable { mutableStateOf(0) }
+ val preferredSkinToneIndex by viewModel.preferredSkinToneIndex.collectAsState()
+
+ // Debounce search input to avoid per-keystroke filtering of 1870 emojis
+ LaunchedEffect(searchQuery) {
+ if (searchQuery.isBlank()) {
+ debouncedQuery = ""
+ } else {
+ delay(SEARCH_DEBOUNCE_MS)
+ debouncedQuery = searchQuery
+ }
+ }
val recentEmojis by
remember(viewModel.customEmojiFrequency) { derivedStateOf { parseRecents(viewModel.customEmojiFrequency) } }
- BottomSheetDialog(onDismiss = onDismiss, modifier = Modifier.fillMaxHeight(fraction = .55f)) {
- EmojiPickerContent(
- searchQuery = searchQuery,
- onSearchQueryChange = { searchQuery = it },
- selectedCategoryIndex = selectedCategoryIndex,
- onCategorySelected = { selectedCategoryIndex = it },
- selectedEmojis = selectedEmojis,
- recentEmojis = recentEmojis,
- onEmojiSelected = { emoji ->
- recordSelection(emoji, viewModel)
- onDismiss()
- onConfirm(emoji)
- },
- )
+ ModalBottomSheet(
+ onDismissRequest = onDismiss,
+ sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
+ ) {
+ if (isLoaded) {
+ EmojiPickerContent(
+ searchQuery = searchQuery,
+ debouncedQuery = debouncedQuery,
+ onSearchQueryChange = { searchQuery = it },
+ selectedCategoryIndex = selectedCategoryIndex,
+ onCategorySelected = { selectedCategoryIndex = it },
+ selectedEmojis = selectedEmojis,
+ recentEmojis = recentEmojis,
+ categories = viewModel.categories,
+ allEmojis = viewModel.allEmojis,
+ preferredSkinToneIndex = preferredSkinToneIndex,
+ onSkinToneSelect = { viewModel.setPreferredSkinTone(it) },
+ onEmojiSelected = { emoji ->
+ recordSelection(emoji, viewModel)
+ onDismiss()
+ onConfirm(emoji)
+ },
+ )
+ } else {
+ Box(modifier = Modifier.fillMaxWidth().height(200.dp), contentAlignment = Alignment.Center) {
+ CircularProgressIndicator()
+ }
+ }
}
}
@@ -171,11 +206,16 @@ fun rememberQuickReactions(count: Int = DEFAULT_QUICK_REACTION_COUNT): List<Stri
@Suppress("LongParameterList")
private fun EmojiPickerContent(
searchQuery: String,
+ debouncedQuery: String,
onSearchQueryChange: (String) -> Unit,
selectedCategoryIndex: Int,
onCategorySelected: (Int) -> Unit,
selectedEmojis: Set<String>,
recentEmojis: List<String>,
+ categories: List<EmojiCategory>,
+ allEmojis: List<Emoji>,
+ preferredSkinToneIndex: Int,
+ onSkinToneSelect: (Int) -> Unit,
onEmojiSelected: (String) -> Unit,
) {
Column {
@@ -186,15 +226,20 @@ private fun EmojiPickerContent(
selectedIndex = selectedCategoryIndex,
onCategorySelected = onCategorySelected,
hasRecents = recentEmojis.isNotEmpty(),
+ categories = categories,
)
}
EmojiGrid(
- searchQuery = searchQuery,
+ searchQuery = debouncedQuery,
selectedCategoryIndex = selectedCategoryIndex,
onCategoryChanged = onCategorySelected,
selectedEmojis = selectedEmojis,
recentEmojis = recentEmojis,
+ categories = categories,
+ allEmojis = allEmojis,
+ preferredSkinToneIndex = preferredSkinToneIndex,
+ onSkinToneSelect = onSkinToneSelect,
onEmojiSelected = onEmojiSelected,
)
}
@@ -246,9 +291,14 @@ private fun SearchBar(query: String, onQueryChange: (String) -> Unit) {
// ── Category Tabs ──────────────────────────────────────────────────────────────
@Composable
-private fun CategoryTabStrip(selectedIndex: Int, onCategorySelected: (Int) -> Unit, hasRecents: Boolean) {
+private fun CategoryTabStrip(
+ selectedIndex: Int,
+ onCategorySelected: (Int) -> Unit,
+ hasRecents: Boolean,
+ categories: List<EmojiCategory>,
+) {
val tabOffset = if (hasRecents) 1 else 0
- val totalTabs = EmojiData.categories.size + tabOffset
+ val totalTabs = categories.size + tabOffset
PrimaryScrollableTabRow(
selectedTabIndex = selectedIndex,
@@ -263,10 +313,7 @@ private fun CategoryTabStrip(selectedIndex: Int, onCategorySelected: (Int) -> Un
selected = selectedIndex == index,
onClick = { onCategorySelected(index) },
text = {
- Text(
- text = if (isRecents) "\uD83D\uDD50" else EmojiData.categories[index - tabOffset].icon,
- fontSize = 18.sp,
- )
+ Text(text = if (isRecents) "\uD83D\uDD50" else categories[index - tabOffset].icon, fontSize = 18.sp)
},
)
}
@@ -283,13 +330,20 @@ private fun EmojiGrid(
onCategoryChanged: (Int) -> Unit,
selectedEmojis: Set<String>,
recentEmojis: List<String>,
+ categories: List<EmojiCategory>,
+ allEmojis: List<Emoji>,
+ preferredSkinToneIndex: Int,
+ onSkinToneSelect: (Int) -> Unit,
onEmojiSelected: (String) -> Unit,
) {
val gridState = rememberLazyGridState()
val hasRecents = recentEmojis.isNotEmpty()
val tabOffset = if (hasRecents) 1 else 0
- val gridItems: List<GridItem> = remember(searchQuery, recentEmojis) { buildGridItems(searchQuery, recentEmojis) }
+ val gridItems: List<GridItem> =
+ remember(searchQuery, recentEmojis, categories, allEmojis) {
+ buildGridItems(searchQuery, recentEmojis, categories, allEmojis)
+ }
var animationTargetIndex by remember { mutableStateOf<Int?>(null) }
// Scroll to category when tab changes
@@ -302,7 +356,7 @@ private fun EmojiGrid(
RECENTS_HEADER_KEY
} else {
val catIndex = selectedCategoryIndex - tabOffset
- if (catIndex in EmojiData.categories.indices) {
+ if (catIndex in categories.indices) {
CATEGORY_HEADER_KEY_PREFIX + catIndex
} else {
null
@@ -363,6 +417,8 @@ private fun EmojiGrid(
EmojiCellWithSkinTone(
emoji = item.emoji,
isSelected = selectedEmojis.contains(item.emoji.base),
+ preferredSkinToneIndex = preferredSkinToneIndex,
+ onSkinToneSelect = onSkinToneSelect,
onSelect = onEmojiSelected,
)
}
@@ -392,11 +448,15 @@ private sealed class GridItem(open val key: String) {
}
@Suppress("CyclomaticComplexMethod")
-private fun buildGridItems(searchQuery: String, recentEmojis: List<String>): List<GridItem> = buildList {
+private fun buildGridItems(
+ searchQuery: String,
+ recentEmojis: List<String>,
+ categories: List<EmojiCategory>,
+ allEmojis: List<Emoji>,
+): List<GridItem> = buildList {
if (searchQuery.isNotBlank()) {
val query = searchQuery.lowercase()
- val results =
- EmojiData.all.filter { emoji -> emoji.keywords.any { it.contains(query) } || emoji.base.contains(query) }
+ val results = rankSearchResults(query, allEmojis, recentEmojis)
results.forEachIndexed { i, emoji -> add(GridItem.EmojiCell(emoji, "search_$i")) }
} else {
if (recentEmojis.isNotEmpty()) {
@@ -405,7 +465,7 @@ private fun buildGridItems(searchQuery: String, recentEmojis: List<String>): Lis
add(GridItem.EmojiCell(Emoji(emojiStr), "$RECENTS_KEY_PREFIX$i"))
}
}
- EmojiData.categories.forEachIndexed { catIndex, category ->
+ categories.forEachIndexed { catIndex, category ->
add(GridItem.Header(category.name, "$CATEGORY_HEADER_KEY_PREFIX$catIndex"))
category.emojis.forEachIndexed { emojiIndex, emoji ->
add(GridItem.EmojiCell(emoji, "cat_${catIndex}_$emojiIndex"))
@@ -414,6 +474,47 @@ private fun buildGridItems(searchQuery: String, recentEmojis: List<String>): Lis
}
}
+/**
+ * Ranks search results using prefix-weighted scoring (inspired by Signal's approach). Exact keyword matches score
+ * highest, then prefix matches, then substring matches. Recently-used emoji matching the query get a boost.
+ */
+private fun rankSearchResults(query: String, allEmojis: List<Emoji>, recentEmojis: List<String>): List<Emoji> {
+ val recentSet = recentEmojis.toSet()
+
+ data class ScoredEmoji(val emoji: Emoji, val score: Float)
+
+ return allEmojis
+ .mapNotNull { emoji ->
+ val score = scoreEmoji(emoji, query, recentSet)
+ if (score > 0f) ScoredEmoji(emoji, score) else null
+ }
+ .sortedByDescending { it.score }
+ .map { it.emoji }
+}
+
+private const val SCORE_EXACT_MATCH = 10f
+private const val SCORE_PREFIX_MATCH = 4f
+private const val SCORE_SUBSTRING_MATCH = 1f
+private const val SCORE_BASE_CONTAINS = 0.5f
+private const val SCORE_RECENT_BOOST = 3f
+
+private fun scoreEmoji(emoji: Emoji, query: String, recentSet: Set<String>): Float {
+ var score = 0f
+
+ for (keyword in emoji.keywords) {
+ when {
+ keyword == query -> score += SCORE_EXACT_MATCH
+ keyword.startsWith(query) -> score += SCORE_PREFIX_MATCH
+ keyword.contains(query) -> score += SCORE_SUBSTRING_MATCH
+ }
+ }
+
+ if (emoji.base.contains(query)) score += SCORE_BASE_CONTAINS
+ if (score > 0f && emoji.base in recentSet) score += SCORE_RECENT_BOOST
+
+ return score
+}
+
// ── Cell Components ────────────────────────────────────────────────────────────
@Composable
@@ -428,14 +529,23 @@ private fun SectionHeader(title: String) {
/**
* An emoji grid cell that supports:
- * - **Tap** → select the emoji (with default skin tone)
+ * - **Tap** → select the emoji with the user's preferred skin tone applied
* - **Long-press** → if the emoji supports skin tones, show a popup with 6 Fitzpatrick variants
* - **Selected highlight** → tinted background when the emoji is in [isSelected]
*/
@OptIn(ExperimentalFoundationApi::class)
@Composable
-private fun EmojiCellWithSkinTone(emoji: Emoji, isSelected: Boolean, onSelect: (String) -> Unit) {
+@Suppress("LongParameterList")
+private fun EmojiCellWithSkinTone(
+ emoji: Emoji,
+ isSelected: Boolean,
+ preferredSkinToneIndex: Int,
+ onSkinToneSelect: (Int) -> Unit,
+ onSelect: (String) -> Unit,
+) {
var showSkinTonePopup by rememberSaveable { mutableStateOf(false) }
+ val preferredTone = SkinTone.entries.getOrElse(preferredSkinToneIndex) { SkinTone.DEFAULT }
+ val displayEmoji = if (emoji.supportsSkinTone) emoji.withSkinTone(preferredTone) else emoji.base
Box {
Box(
@@ -450,7 +560,7 @@ private fun EmojiCellWithSkinTone(emoji: Emoji, isSelected: Boolean, onSelect: (
},
)
.combinedClickable(
- onClick = { onSelect(emoji.base) },
+ onClick = { onSelect(displayEmoji) },
onLongClick =
if (emoji.supportsSkinTone) {
{ showSkinTonePopup = true }
@@ -460,7 +570,7 @@ private fun EmojiCellWithSkinTone(emoji: Emoji, isSelected: Boolean, onSelect: (
),
contentAlignment = Alignment.Center,
) {
- Text(text = emoji.base, fontSize = EMOJI_FONT_SIZE.sp, textAlign = TextAlign.Center)
+ Text(text = displayEmoji, fontSize = EMOJI_FONT_SIZE.sp, textAlign = TextAlign.Center)
// Small dot indicator for skin-tone-capable emoji
if (emoji.supportsSkinTone) {
Box(
@@ -476,8 +586,9 @@ private fun EmojiCellWithSkinTone(emoji: Emoji, isSelected: Boolean, onSelect: (
if (showSkinTonePopup) {
SkinTonePopup(
emoji = emoji,
- onSelect = { variant ->
+ onSelect = { variant, toneIndex ->
showSkinTonePopup = false
+ onSkinToneSelect(toneIndex)
onSelect(variant)
},
onDismiss = { showSkinTonePopup = false },
@@ -489,7 +600,7 @@ private fun EmojiCellWithSkinTone(emoji: Emoji, isSelected: Boolean, onSelect: (
// ── Skin Tone Popup ────────────────────────────────────────────────────────────
@Composable
-private fun SkinTonePopup(emoji: Emoji, onSelect: (String) -> Unit, onDismiss: () -> Unit) {
+private fun SkinTonePopup(emoji: Emoji, onSelect: (String, Int) -> Unit, onDismiss: () -> Unit) {
Popup(alignment = Alignment.TopCenter, onDismissRequest = onDismiss) {
Surface(
shape = RoundedCornerShape(12.dp),
@@ -499,10 +610,11 @@ private fun SkinTonePopup(emoji: Emoji, onSelect: (String) -> Unit, onDismiss: (
modifier = Modifier.widthIn(max = 280.dp),
) {
Row(modifier = Modifier.padding(6.dp), horizontalArrangement = Arrangement.spacedBy(2.dp)) {
- SkinTone.entries.forEach { tone ->
+ SkinTone.entries.forEachIndexed { index, tone ->
val variant = emoji.withSkinTone(tone)
Box(
- modifier = Modifier.size(40.dp).clip(RoundedCornerShape(8.dp)).clickable { onSelect(variant) },
+ modifier =
+ Modifier.size(40.dp).clip(RoundedCornerShape(8.dp)).clickable { onSelect(variant, index) },
contentAlignment = Alignment.Center,
) {
Text(text = variant, fontSize = 22.sp)
@@ -552,3 +664,89 @@ private fun recordSelection(emoji: String, viewModel: EmojiPickerViewModel) {
viewModel.customEmojiFrequency =
freq.entries.joinToString(SPLIT_CHAR) { "${it.key}$KEY_VALUE_DELIMITER${it.value}" }
}
+
+// ── Previews ───────────────────────────────────────────────────────────────────
+
+private val PREVIEW_CATEGORIES =
+ listOf(
+ EmojiCategory(
+ "Smileys & Emotion",
+ "😀",
+ listOf(
+ Emoji("😀", listOf("grinning", "face", "smile")),
+ Emoji("😃", listOf("smiley", "face", "happy")),
+ Emoji("😄", listOf("smile", "happy", "joy")),
+ Emoji("😁", listOf("grin", "happy")),
+ Emoji("😆", listOf("laughing", "satisfied")),
+ Emoji("😅", listOf("sweat", "smile", "hot")),
+ Emoji("🤣", listOf("rofl", "laughing", "floor")),
+ Emoji("😂", listOf("joy", "tears", "laugh")),
+ Emoji("🙂", listOf("slightly", "smile")),
+ Emoji("😉", listOf("wink", "face")),
+ Emoji("😊", listOf("blush", "happy", "smile")),
+ Emoji("😇", listOf("angel", "innocent", "halo")),
+ ),
+ ),
+ EmojiCategory(
+ "People & Body",
+ "👋",
+ listOf(
+ Emoji("👋", listOf("wave", "hand", "hello"), supportsSkinTone = true),
+ Emoji("🤚", listOf("raised", "back", "hand"), supportsSkinTone = true),
+ Emoji("🖐️", listOf("hand", "splayed", "fingers"), supportsSkinTone = true),
+ Emoji("✋", listOf("hand", "high five", "stop"), supportsSkinTone = true),
+ Emoji("👍", listOf("thumbs up", "approve", "yes"), supportsSkinTone = true),
+ Emoji("👎", listOf("thumbs down", "disapprove", "no"), supportsSkinTone = true),
+ Emoji("👏", listOf("clap", "applause"), supportsSkinTone = true),
+ Emoji("🙌", listOf("raised", "hands", "celebration"), supportsSkinTone = true),
+ ),
+ ),
+ )
+
+@Suppress("UnusedPrivateMember", "PreviewPublic")
+@PreviewLightDark
+@Composable
+fun EmojiPickerContentPreview() {
+ AppTheme {
+ Surface {
+ EmojiPickerContent(
+ searchQuery = "",
+ debouncedQuery = "",
+ onSearchQueryChange = {},
+ selectedCategoryIndex = 0,
+ onCategorySelected = {},
+ selectedEmojis = setOf("😀", "👍"),
+ recentEmojis = listOf("😀", "❤️", "👍", "🔥", "😂", "🙏"),
+ categories = PREVIEW_CATEGORIES,
+ allEmojis = PREVIEW_CATEGORIES.flatMap { it.emojis },
+ preferredSkinToneIndex = 0,
+ onSkinToneSelect = {},
+ onEmojiSelected = {},
+ )
+ }
+ }
+}
+
+@Suppress("UnusedPrivateMember", "PreviewPublic")
+@PreviewLightDark
+@Composable
+fun EmojiPickerSearchPreview() {
+ AppTheme {
+ Surface {
+ EmojiPickerContent(
+ searchQuery = "smile",
+ debouncedQuery = "smile",
+ onSearchQueryChange = {},
+ selectedCategoryIndex = 0,
+ onCategorySelected = {},
+ selectedEmojis = emptySet(),
+ recentEmojis = listOf("😀", "❤️", "👍"),
+ categories = PREVIEW_CATEGORIES,
+ allEmojis = PREVIEW_CATEGORIES.flatMap { it.emojis },
+ preferredSkinToneIndex = 0,
+ onSkinToneSelect = {},
+ onEmojiSelected = {},
+ )
+ }
+ }
+}

diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/emoji/EmojiPickerViewModel.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/emoji/EmojiPickerViewModel.kt
index 1d7269b64a..4feb31125c 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/emoji/EmojiPickerViewModel.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/emoji/EmojiPickerViewModel.kt
@@ -17,11 +17,43 @@
package org.meshtastic.core.ui.emoji
import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
import org.koin.core.annotation.KoinViewModel
import org.meshtastic.core.repository.CustomEmojiPrefs
@KoinViewModel
-class EmojiPickerViewModel(private val customEmojiPrefs: CustomEmojiPrefs) : ViewModel() {
+internal class EmojiPickerViewModel(
+ private val customEmojiPrefs: CustomEmojiPrefs,
+ private val emojiRepository: EmojiRepository,
+) : ViewModel() {
+
+ private val _isLoaded = MutableStateFlow(false)
+ val isLoaded: StateFlow<Boolean> = _isLoaded
+
+ /** Emoji categories, available after [isLoaded] emits true. */
+ val categories: List<EmojiCategory>
+ get() = emojiRepository.categories
+
+ /** Flat list of all emojis, available after [isLoaded] emits true. */
+ val allEmojis: List<Emoji>
+ get() = emojiRepository.all
+
+ init {
+ viewModelScope.launch {
+ emojiRepository.preload()
+ _isLoaded.value = true
+ }
+ }
+
+ /** User's preferred skin tone (persisted across sessions). */
+ val preferredSkinToneIndex: StateFlow<Int> = customEmojiPrefs.preferredSkinToneIndex
+
+ fun setPreferredSkinTone(index: Int) {
+ customEmojiPrefs.setPreferredSkinToneIndex(index)
+ }
var customEmojiFrequency: String?
get() = customEmojiPrefs.customEmojiFrequency.value

diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/emoji/EmojiRepository.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/emoji/EmojiRepository.kt
new file mode 100644
index 0000000000..d6b587f412
--- /dev/null
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/emoji/EmojiRepository.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright (c) 2026 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.meshtastic.core.ui.emoji
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.Json
+import meshtasticandroid.core.ui.generated.resources.Res
+import org.koin.core.annotation.Single
+
+/** A single emoji entry with optional skin-tone support and search keywords. */
+internal data class Emoji(
+ val base: String,
+ val keywords: List<String> = emptyList(),
+ val supportsSkinTone: Boolean = false,
+)
+
+/** A named category of emojis with an icon emoji for the tab. */
+internal data class EmojiCategory(val name: String, val icon: String, val emojis: List<Emoji>)
+
+/** Unicode skin tone modifiers (Fitzpatrick scale). */
+internal enum class SkinTone(val modifier: String, val label: String, val preview: String) {
+ DEFAULT("", "Default", "👋"),
+ LIGHT("\uD83C\uDFFB", "Light", "👋🏻"),
+ MEDIUM_LIGHT("\uD83C\uDFFC", "Medium-Light", "👋🏼"),
+ MEDIUM("\uD83C\uDFFD", "Medium", "👋🏽"),
+ MEDIUM_DARK("\uD83C\uDFFE", "Medium-Dark", "👋🏾"),
+ DARK("\uD83C\uDFFF", "Dark", "👋🏿"),
+}
+
+/**
+ * Applies a skin tone modifier to a base emoji string. Only works correctly for single-codepoint emojis that support
+ * skin tones.
+ */
+internal fun Emoji.withSkinTone(tone: SkinTone): String {
+ if (!supportsSkinTone || tone == SkinTone.DEFAULT) return base
+ val firstChar = base[0]
+ val charCount = if (firstChar.isHighSurrogate() && base.length > 1) 2 else 1
+ val baseChar = base.substring(0, charCount)
+ val after = base.substring(charCount)
+ return baseChar + tone.modifier + after
+}
+
+// ── JSON models (compact wire format) ──────────────────────────────────────────
+
+@Serializable
+private data class EmojiJson(
+ @SerialName("b") val base: String,
+ @SerialName("k") val keywords: List<String> = emptyList(),
+ @SerialName("s") val supportsSkinTone: Boolean = false,
+)
+
+@Serializable private data class CategoryJson(val name: String, val icon: String, val emojis: List<EmojiJson>)
+
+@Serializable private data class EmojiDataJson(val version: String, val categories: List<CategoryJson>)
+
+// ── Repository ─────────────────────────────────────────────────────────────────
+
+/**
+ * Provides access to the emoji catalog loaded from a bundled JSON resource.
+ *
+ * Data is loaded lazily on first access and cached for the lifetime of the app. The JSON resource contains Unicode 16.0
+ * emoji data with CLDR-based keywords.
+ */
+@Single
+internal class EmojiRepository {
+
+ private var cached: Pair<List<EmojiCategory>, List<Emoji>>? = null
+
+ val categories: List<EmojiCategory>
+ get() = ensureLoaded().first
+
+ val all: List<Emoji>
+ get() = ensureLoaded().second
+
+ suspend fun preload() {
+ if (cached != null) return
+ val bytes = Res.readBytes("files/emoji-data.json")
+ val json = Json { ignoreUnknownKeys = true }
+ val data = json.decodeFromString<EmojiDataJson>(bytes.decodeToString())
+ val cats =
+ data.categories.map { cat ->
+ EmojiCategory(
+ name = cat.name,
+ icon = cat.icon,
+ emojis =
+ cat.emojis.map { e ->
+ Emoji(base = e.base, keywords = e.keywords, supportsSkinTone = e.supportsSkinTone)
+ },
+ )
+ }
+ cached = cats to cats.flatMap { it.emojis }
+ }
+
+ private fun ensureLoaded(): Pair<List<EmojiCategory>, List<Emoji>> =
+ cached ?: error("EmojiRepository not loaded. Call preload() before accessing data.")
+}

diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/theme/Theme.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/theme/Theme.kt
index 5b3cffcfda..acce06a76a 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/theme/Theme.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/theme/Theme.kt
@@ -15,6 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@file:Suppress("MatchingDeclarationName")
+@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
package org.meshtastic.core.ui.theme
@@ -127,12 +128,9 @@ fun AppTheme(
null
} ?: if (darkTheme) darkScheme else lightScheme
- MaterialExpressiveTheme(
- colorScheme = colorScheme,
- typography = AppTypography,
- motionScheme = expressive(),
- content = content,
- )
+ MaterialExpressiveTheme(colorScheme = colorScheme, typography = AppTypography, motionScheme = expressive()) {
+ content()
+ }
}
const val MODE_DYNAMIC = 6969420

diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/theme/Type.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/theme/Type.kt
index c3c53125ba..b7b28717a9 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/theme/Type.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/theme/Type.kt
@@ -14,8 +14,40 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
+
package org.meshtastic.core.ui.theme
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
-val AppTypography = Typography()
+/**
+ * Complete expressive typescale for the Meshtastic application.
+ *
+ * `bodyLarge` and `bodyMedium` enforce a minimum of 16.sp per design standards §5. Emphasized variants (e.g.
+ * `titleMediumEmphasized`, `bodyLargeEmphasized`) are auto-generated by [MaterialExpressiveTheme] based on these base
+ * styles.
+ */
+val AppTypography =
+ Typography(
+ bodyLarge =
+ TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.5.sp,
+ ),
+ bodyMedium =
+ TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp,
+ lineHeight = 22.sp,
+ letterSpacing = 0.25.sp,
+ ),
+ )

diff --git a/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/emoji/EmojiPickerViewModelTest.kt b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/emoji/EmojiPickerViewModelTest.kt
index 12441b4298..041ea3820d 100644
--- a/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/emoji/EmojiPickerViewModelTest.kt
+++ b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/emoji/EmojiPickerViewModelTest.kt
@@ -33,12 +33,15 @@ class EmojiPickerViewModelTest {
private lateinit var viewModel: EmojiPickerViewModel
private val customEmojiPrefs: CustomEmojiPrefs = mock(MockMode.autofill)
+ private val emojiRepository = EmojiRepository()
private val frequencyFlow = MutableStateFlow<String?>(null)
+ private val skinToneFlow = MutableStateFlow(0)
@BeforeTest
fun setUp() {
every { customEmojiPrefs.customEmojiFrequency } returns frequencyFlow
- viewModel = EmojiPickerViewModel(customEmojiPrefs)
+ every { customEmojiPrefs.preferredSkinToneIndex } returns skinToneFlow
+ viewModel = EmojiPickerViewModel(customEmojiPrefs, emojiRepository)
}
@Test

diff --git a/core/ui/src/iosMain/kotlin/org/meshtastic/core/ui/component/NoopStubs.kt b/core/ui/src/iosMain/kotlin/org/meshtastic/core/ui/component/NoopStubs.kt
index e18ffd84c3..ff88d00eb8 100644
--- a/core/ui/src/iosMain/kotlin/org/meshtastic/core/ui/component/NoopStubs.kt
+++ b/core/ui/src/iosMain/kotlin/org/meshtastic/core/ui/component/NoopStubs.kt
@@ -16,10 +16,6 @@
*/
package org.meshtastic.core.ui.component
-import androidx.compose.runtime.Composable
-
-@Composable actual fun rememberTimeTickWithLifecycle(): Long = 0L
-
internal actual fun <T : Enum<T>> enumEntriesOf(selectedItem: T): List<T> = emptyList()
internal actual fun Enum<*>.isDeprecatedEnumEntry(): Boolean = false

diff --git a/core/ui/src/jvmMain/kotlin/org/meshtastic/core/ui/component/TimeTickWithLifecycle.kt b/core/ui/src/jvmMain/kotlin/org/meshtastic/core/ui/component/TimeTickWithLifecycle.kt
deleted file mode 100644
index 4d46554190..0000000000
--- a/core/ui/src/jvmMain/kotlin/org/meshtastic/core/ui/component/TimeTickWithLifecycle.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Copyright (c) 2026 Meshtastic LLC
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
-package org.meshtastic.core.ui.component
-
-import androidx.compose.runtime.Composable
-import org.meshtastic.core.common.util.nowMillis
-
-/** JVM implementation — returns the current epoch millis (no lifecycle-based updates on Desktop). */
-@Composable actual fun rememberTimeTickWithLifecycle(): Long = nowMillis

diff --git a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/ConnectingDeviceInfo.kt b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/ConnectingDeviceInfo.kt
index 9c6897c533..e27939314e 100644
--- a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/ConnectingDeviceInfo.kt
+++ b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/ConnectingDeviceInfo.kt
@@ -14,6 +14,8 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
+
package org.meshtastic.feature.connections.ui.components
import androidx.compose.foundation.layout.Arrangement
@@ -22,7 +24,8 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
-import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.CircularWavyProgressIndicator
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -66,7 +69,7 @@ fun ConnectingDeviceInfo(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
- CircularProgressIndicator(modifier = Modifier.size(40.dp))
+ CircularWavyProgressIndicator(modifier = Modifier.size(40.dp))
Column {
Text(text = deviceName, style = MaterialTheme.typography.headlineSmall)

diff --git a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/ConnectionActionButton.kt b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/ConnectionActionButton.kt
index e383494df7..f964db65d4 100644
--- a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/ConnectionActionButton.kt
+++ b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/ConnectionActionButton.kt
@@ -14,12 +14,15 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
+
package org.meshtastic.feature.connections.ui.components
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedButton
@@ -62,6 +65,7 @@ fun ConnectionActionButton(
onClick = onClick,
modifier = modifier,
enabled = enabled,
+ shapes = ButtonDefaults.shapes(),
contentPadding = ButtonDefaults.ButtonWithIconContentPadding,
) {
content()
@@ -72,6 +76,7 @@ fun ConnectionActionButton(
onClick = onClick,
modifier = modifier,
enabled = enabled,
+ shapes = ButtonDefaults.shapes(),
contentPadding = ButtonDefaults.ButtonWithIconContentPadding,
) {
content()
@@ -82,6 +87,7 @@ fun ConnectionActionButton(
onClick = onClick,
modifier = modifier,
enabled = enabled,
+ shapes = ButtonDefaults.shapes(),
contentPadding = ButtonDefaults.ButtonWithIconContentPadding,
) {
content()

diff --git a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/CurrentlyConnectedInfo.kt b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/CurrentlyConnectedInfo.kt
index 03a25a3753..b0785fe967 100644
--- a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/CurrentlyConnectedInfo.kt
+++ b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/CurrentlyConnectedInfo.kt
@@ -14,6 +14,8 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
+
package org.meshtastic.feature.connections.ui.components
import androidx.compose.foundation.layout.Arrangement
@@ -21,6 +23,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -95,7 +98,7 @@ fun CurrentlyConnectedInfo(
NodeChip(node = node, onClick = { onNavigateToNodeDetails(it.num) })
Column(modifier = Modifier.weight(1f, fill = true)) {
- Text(text = node.user.long_name, style = MaterialTheme.typography.titleMedium)
+ Text(text = node.user.long_name, style = MaterialTheme.typography.titleMediumEmphasized)
node.metadata
?.firmware_version

diff --git a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/DeviceListItem.kt b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/DeviceListItem.kt
index cebc40724d..dcc51e1a8a 100644
--- a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/DeviceListItem.kt
+++ b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/DeviceListItem.kt
@@ -14,6 +14,8 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
+
package org.meshtastic.feature.connections.ui.components
import androidx.compose.foundation.ExperimentalFoundationApi
@@ -24,7 +26,8 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.selection.selectable
-import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.CircularWavyProgressIndicator
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
@@ -156,7 +159,7 @@ fun DeviceListItem(
}
if (connectionState is ConnectionState.Connecting) {
- CircularProgressIndicator(modifier = Modifier.size(32.dp))
+ CircularWavyProgressIndicator(modifier = Modifier.size(32.dp))
} else {
RadioButton(selected = connectionState is ConnectionState.Connected, onClick = null)
}

diff --git a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/DeviceSectionHeader.kt b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/DeviceSectionHeader.kt
index 7d4f28005b..f344041c3f 100644
--- a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/DeviceSectionHeader.kt
+++ b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/DeviceSectionHeader.kt
@@ -14,6 +14,8 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
+
package org.meshtastic.feature.connections.ui.components
import androidx.compose.foundation.layout.Arrangement
@@ -21,7 +23,8 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.LinearProgressIndicator
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.material3.LinearWavyProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -60,7 +63,7 @@ fun DeviceSectionHeader(
trailing()
}
if (showProgress) {
- LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
+ LinearWavyProgressIndicator(modifier = Modifier.fillMaxWidth())
}
}
}

diff --git a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateScreen.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateScreen.kt
index e63ab43389..45aa27cd22 100644
--- a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateScreen.kt
+++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateScreen.kt
@@ -14,6 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
@file:Suppress("TooManyFunctions")
package org.meshtastic.feature.firmware
@@ -39,7 +40,6 @@ import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CenterAlignedTopAppBar
-import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.CircularWavyProgressIndicator
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -337,7 +337,7 @@ private fun FirmwareUpdateContent(
@Composable
internal fun VerifyingState() {
- CircularProgressIndicator(modifier = Modifier.size(64.dp))
+ CircularWavyProgressIndicator(modifier = Modifier.size(64.dp))
Spacer(Modifier.height(24.dp))
Text(stringResource(Res.string.firmware_update_verifying), style = MaterialTheme.typography.titleMedium)
Spacer(Modifier.height(8.dp))
@@ -352,7 +352,7 @@ internal fun VerifyingState() {
@Composable
internal fun CheckingState() {
- CircularProgressIndicator(modifier = Modifier.size(64.dp))
+ CircularWavyProgressIndicator(modifier = Modifier.size(64.dp))
Spacer(Modifier.height(24.dp))
Text(stringResource(Res.string.firmware_update_checking), style = MaterialTheme.typography.bodyLarge)
}
@@ -763,7 +763,7 @@ private fun AwaitingFileSaveState(state: FirmwareUpdateState.AwaitingFileSave, o
)
}
- CircularProgressIndicator(modifier = Modifier.size(64.dp))
+ CircularWavyProgressIndicator(modifier = Modifier.size(64.dp))
Spacer(Modifier.height(24.dp))
Text(
stringResource(Res.string.firmware_update_save_dfu_file),

diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/MapControlsOverlay.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/MapControlsOverlay.kt
index 3b20211c89..1445424b3f 100644
--- a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/MapControlsOverlay.kt
+++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/MapControlsOverlay.kt
@@ -14,12 +14,14 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
+
package org.meshtastic.feature.map.component
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
-import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.CircularWavyProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FloatingToolbarDefaults
import androidx.compose.material3.HorizontalFloatingToolbar
@@ -103,7 +105,7 @@ fun MapControlsOverlay(
if (showRefresh) {
if (isRefreshing) {
Box(modifier = Modifier.padding(8.dp)) {
- CircularProgressIndicator(modifier = Modifier.size(24.dp), strokeWidth = 2.dp)
+ CircularWavyProgressIndicator(modifier = Modifier.size(24.dp))
}
} else {
MapButton(

diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageScreenComponents.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageScreenComponents.kt
index dba6bb4937..4d89b342c2 100644
--- a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageScreenComponents.kt
+++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageScreenComponents.kt
@@ -15,6 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@file:Suppress("TooManyFunctions")
+@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
package org.meshtastic.feature.messaging.component
@@ -36,13 +37,16 @@ import androidx.compose.material3.Badge
import androidx.compose.material3.BadgedBox
import androidx.compose.material3.Button
import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuGroup
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.MenuDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
@@ -389,13 +393,17 @@ private fun OverFlowMenu(
) {
if (expanded) {
DropdownMenu(expanded = expanded, onDismissRequest = onDismiss) {
- QuickChatToggleMenuItem(showQuickChat, onDismiss, onToggleQuickChat)
- QuickChatOptionsMenuItem(onDismiss, onNavigateToQuickChatOptions)
- if (filteredCount > 0 && !filteringDisabled) {
- FilteredMessagesMenuItem(showFiltered, filteredCount, onDismiss, onToggleShowFiltered)
+ DropdownMenuGroup(shapes = MenuDefaults.groupShapes()) {
+ QuickChatToggleMenuItem(showQuickChat, onDismiss, onToggleQuickChat)
+ QuickChatOptionsMenuItem(onDismiss, onNavigateToQuickChatOptions)
+ }
+ DropdownMenuGroup(shapes = MenuDefaults.groupShapes()) {
+ if (filteredCount > 0 && !filteringDisabled) {
+ FilteredMessagesMenuItem(showFiltered, filteredCount, onDismiss, onToggleShowFiltered)
+ }
+ FilterToggleMenuItem(filteringDisabled, onDismiss, onToggleFilteringDisabled)
+ FilterSettingsMenuItem(onDismiss, onNavigateToFilterSettings)
}
- FilterToggleMenuItem(filteringDisabled, onDismiss, onToggleFilteringDisabled)
- FilterSettingsMenuItem(onDismiss, onNavigateToFilterSettings)
}
}
}

diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/Reaction.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/Reaction.kt
index c08298e297..d11fc1ae9e 100644
--- a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/Reaction.kt
+++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/Reaction.kt
@@ -26,7 +26,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@@ -34,11 +33,14 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
+import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -68,7 +70,6 @@ import org.meshtastic.core.resources.message_status_queued
import org.meshtastic.core.resources.message_status_unknown
import org.meshtastic.core.resources.react
import org.meshtastic.core.resources.you
-import org.meshtastic.core.ui.component.BottomSheetDialog
import org.meshtastic.core.ui.component.Rssi
import org.meshtastic.core.ui.component.Snr
import org.meshtastic.core.ui.emoji.EmojiPickerDialog
@@ -187,6 +188,7 @@ internal fun AddReactionButton(modifier: Modifier = Modifier, onSendReaction: (S
}
}
+@OptIn(ExperimentalMaterial3Api::class)
@Suppress("LongMethod", "CyclomaticComplexity", "CyclomaticComplexMethod")
@Composable
internal fun ReactionDialog(
@@ -194,7 +196,10 @@ internal fun ReactionDialog(
onDismiss: () -> Unit = {},
myId: String? = null,
onResend: (Reaction) -> Unit = {},
-) = BottomSheetDialog(onDismiss = onDismiss, modifier = Modifier.fillMaxHeight(fraction = .3f)) {
+) = ModalBottomSheet(
+ onDismissRequest = onDismiss,
+ sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
+) {
val groupedEmojis = reactions.groupBy { it.emoji }
var selectedEmoji by remember { mutableStateOf<String?>(null) }
val filteredReactions = selectedEmoji?.let { groupedEmojis[it] ?: emptyList() } ?: reactions

diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactItem.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactItem.kt
index b96a596980..4979845486 100644
--- a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactItem.kt
+++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactItem.kt
@@ -14,6 +14,8 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
+
package org.meshtastic.feature.messaging.ui.contact
import androidx.compose.animation.AnimatedVisibility
@@ -35,6 +37,7 @@ import androidx.compose.material3.AssistChip
import androidx.compose.material3.AssistChipDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@@ -141,7 +144,7 @@ private fun ContactHeader(
Text(
modifier = Modifier.padding(start = 8.dp).weight(1f),
- style = MaterialTheme.typography.bodyLarge,
+ style = MaterialTheme.typography.bodyLargeEmphasized,
fontWeight = FontWeight.Medium,
overflow = TextOverflow.Ellipsis,
maxLines = 1,

diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/Contacts.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/Contacts.kt
index 2792dfb548..63389dc5a6 100644
--- a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/Contacts.kt
+++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/Contacts.kt
@@ -14,6 +14,8 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
+
package org.meshtastic.feature.messaging.ui.contact
import androidx.compose.foundation.layout.Box
@@ -27,8 +29,9 @@ import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.selection.selectable
-import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.CircularWavyProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.RadioButton
@@ -501,7 +504,7 @@ private fun ContactListViewPaged(
val haptic = LocalHapticFeedback.current
Box(modifier = modifier.fillMaxSize()) {
if (contacts.loadState.refresh is LoadState.Loading && contacts.itemCount == 0) {
- CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
+ CircularWavyProgressIndicator(modifier = Modifier.align(Alignment.Center))
} else {
ContactListContentInternal(
contacts = contacts,
@@ -621,7 +624,7 @@ private fun LazyListScope.contactListAppendLoadingItem(contacts: LazyPagingItems
if (contacts.loadState.append is LoadState.Loading) {
item {
Box(modifier = Modifier.fillMaxWidth().padding(16.dp)) {
- CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
+ CircularWavyProgressIndicator(modifier = Modifier.align(Alignment.Center))
}
}
}

diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/AdministrationSection.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/AdministrationSection.kt
index 80c4c132be..a70619c175 100644
--- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/AdministrationSection.kt
+++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/AdministrationSection.kt
@@ -14,6 +14,8 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
+
package org.meshtastic.feature.node.component
import androidx.compose.animation.AnimatedVisibility
@@ -23,7 +25,8 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.AssistChip
import androidx.compose.material3.AssistChipDefaults
-import androidx.compose.material3.LinearProgressIndicator
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.material3.LinearWavyProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@@ -168,7 +171,7 @@ private fun RemoteAdminListItem(
)
AnimatedVisibility(visible = isEnsuringSession) {
Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 4.dp)) {
- LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
+ LinearWavyProgressIndicator(modifier = Modifier.fillMaxWidth())
androidx.compose.material3.Text(
text = stringResource(Res.string.establishing_session),
style = MaterialTheme.typography.bodySmall,

diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/CompassBottomSheet.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/CompassBottomSheet.kt
index 4763caf3cd..01e7a0672b 100644
--- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/CompassBottomSheet.kt
+++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/CompassBottomSheet.kt
@@ -14,6 +14,8 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
+
package org.meshtastic.feature.node.component
import androidx.compose.foundation.Canvas
@@ -26,6 +28,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Button
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
@@ -99,7 +102,11 @@ fun CompassSheetContent(
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(text = stringResource(Res.string.compass_title), style = MaterialTheme.typography.headlineSmall)
- Text(text = uiState.targetName, style = MaterialTheme.typography.titleMedium, color = uiState.targetColor)
+ Text(
+ text = uiState.targetName,
+ style = MaterialTheme.typography.titleMediumEmphasized,
+ color = uiState.targetColor,
+ )
CompassDial(
heading = uiState.heading,

diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/DeviceActions.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/DeviceActions.kt
index 4fe65e9666..1e7e971b8c 100644
--- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/DeviceActions.kt
+++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/DeviceActions.kt
@@ -26,10 +26,10 @@ import androidx.compose.foundation.layout.width
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
-import androidx.compose.material3.IconToggleButton
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.OutlinedIconToggleButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@@ -136,7 +136,7 @@ private fun PrimaryActionsRow(node: Node, isLocal: Boolean, onAction: (NodeDetai
}
if (!isLocal) {
- IconToggleButton(
+ OutlinedIconToggleButton(
checked = node.isFavorite,
onCheckedChange = { onAction(NodeDetailAction.HandleNodeMenuAction(NodeMenuAction.Favorite(node))) },
) {

diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeContextMenu.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeContextMenu.kt
index 29f526deb3..ec6ef384e4 100644
--- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeContextMenu.kt
+++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeContextMenu.kt
@@ -14,13 +14,18 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
+
package org.meshtastic.feature.node.component
import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuGroup
import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.MenuDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
@@ -59,12 +64,16 @@ fun NodeContextMenu(
onDismiss: () -> Unit,
) {
DropdownMenu(expanded = expanded, onDismissRequest = onDismiss) {
- FavoriteMenuItem(node, onFavorite, onDismiss)
- IgnoreMenuItem(node, onIgnore, onDismiss)
- if (node.capabilities.canMuteNode) {
- MuteMenuItem(node, onMute, onDismiss)
+ DropdownMenuGroup(shapes = MenuDefaults.groupShapes()) {
+ FavoriteMenuItem(node, onFavorite, onDismiss)
+ if (node.capabilities.canMuteNode) {
+ MuteMenuItem(node, onMute, onDismiss)
+ }
+ }
+ DropdownMenuGroup(shapes = MenuDefaults.groupShapes()) {
+ IgnoreMenuItem(node, onIgnore, onDismiss)
+ RemoveMenuItem(node, onRemove, onDismiss)
}
- RemoveMenuItem(node, onRemove, onDismiss)
}
}

diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeFilterTextField.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeFilterTextField.kt
index 88423aab10..b1f2a37991 100644
--- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeFilterTextField.kt
+++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeFilterTextField.kt
@@ -14,6 +14,8 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
+
package org.meshtastic.feature.node.component
import androidx.compose.foundation.background
@@ -31,8 +33,9 @@ import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Checkbox
import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuGroup
import androidx.compose.material3.DropdownMenuItem
-import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
@@ -227,67 +230,69 @@ private fun NodeSortButton(
onDismissRequest = { expanded = false },
modifier = Modifier.background(MaterialTheme.colorScheme.background.copy(alpha = 1f)),
) {
- DropdownMenuTitle(text = stringResource(Res.string.node_sort_title))
+ DropdownMenuGroup(shapes = MenuDefaults.groupShapes()) {
+ DropdownMenuTitle(text = stringResource(Res.string.node_sort_title))
- NodeSortOption.entries.forEach { sort ->
- DropdownMenuRadio(
- text = stringResource(sort.stringRes),
- selected = sort == currentSortOption,
- onClick = { onSortSelect(sort) },
- )
+ NodeSortOption.entries.forEach { sort ->
+ DropdownMenuRadio(
+ text = stringResource(sort.stringRes),
+ selected = sort == currentSortOption,
+ onClick = { onSortSelect(sort) },
+ )
+ }
}
- HorizontalDivider(modifier = Modifier.padding(MenuDefaults.DropdownMenuItemContentPadding))
-
- DropdownMenuTitle(text = stringResource(Res.string.node_filter_title))
+ DropdownMenuGroup(shapes = MenuDefaults.groupShapes()) {
+ DropdownMenuTitle(text = stringResource(Res.string.node_filter_title))
- DropdownMenuCheck(
- text = stringResource(Res.string.node_filter_exclude_infrastructure),
- checked = toggles.excludeInfrastructure,
- onClick = toggles.onToggleExcludeInfrastructure,
- )
+ DropdownMenuCheck(
+ text = stringResource(Res.string.node_filter_exclude_infrastructure),
+ checked = toggles.excludeInfrastructure,
+ onClick = toggles.onToggleExcludeInfrastructure,
+ )
- DropdownMenuCheck(
- text = stringResource(Res.string.node_filter_include_unknown),
- checked = toggles.includeUnknown,
- onClick = toggles.onToggleIncludeUnknown,
- )
+ DropdownMenuCheck(
+ text = stringResource(Res.string.node_filter_include_unknown),
+ checked = toggles.includeUnknown,
+ onClick = toggles.onToggleIncludeUnknown,
+ )
- DropdownMenuCheck(
- text = stringResource(Res.string.node_filter_only_online),
- checked = toggles.onlyOnline,
- onClick = toggles.onToggleOnlyOnline,
- )
+ DropdownMenuCheck(
+ text = stringResource(Res.string.node_filter_only_online),
+ checked = toggles.onlyOnline,
+ onClick = toggles.onToggleOnlyOnline,
+ )
- DropdownMenuCheck(
- text = stringResource(Res.string.node_filter_only_direct),
- checked = toggles.onlyDirect,
- onClick = toggles.onToggleOnlyDirect,
- )
+ DropdownMenuCheck(
+ text = stringResource(Res.string.node_filter_only_direct),
+ checked = toggles.onlyDirect,
+ onClick = toggles.onToggleOnlyDirect,
+ )
- DropdownMenuCheck(
- text = stringResource(Res.string.node_filter_show_ignored),
- checked = toggles.showIgnored,
- onClick = toggles.onToggleShowIgnored,
- trailing =
- if (toggles.ignoredNodeCount > 0) {
- {
- Text(
- text = " (${toggles.ignoredNodeCount})",
- color = MaterialTheme.colorScheme.primary,
- modifier = Modifier.padding(start = 4.dp),
- )
- }
- } else {
- null
- },
- )
+ DropdownMenuCheck(
+ text = stringResource(Res.string.node_filter_show_ignored),
+ checked = toggles.showIgnored,
+ onClick = toggles.onToggleShowIgnored,
+ trailing =
+ if (toggles.ignoredNodeCount > 0) {
+ {
+ Text(
+ text = " (${toggles.ignoredNodeCount})",
+ color = MaterialTheme.colorScheme.primary,
+ modifier = Modifier.padding(start = 4.dp),
+ )
+ }
+ } else {
+ null
+ },
+ )
- DropdownMenuCheck(
- text = stringResource(Res.string.node_filter_exclude_mqtt),
- checked = toggles.excludeMqtt,
- onClick = toggles.onToggleExcludeMqtt,
- )
+ DropdownMenuCheck(
+ text = stringResource(Res.string.node_filter_exclude_mqtt),
+ checked = toggles.excludeMqtt,
+ onClick = toggles.onToggleExcludeMqtt,
+ )
+ }
}
}

diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NotesSection.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NotesSection.kt
index ff93c4a2ef..6347901da7 100644
--- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NotesSection.kt
+++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NotesSection.kt
@@ -14,6 +14,8 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
+
package org.meshtastic.feature.node.component
import androidx.compose.foundation.layout.Column
@@ -25,6 +27,7 @@ import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ElevatedCard
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
@@ -60,7 +63,7 @@ fun NotesSection(node: Node, onSaveNotes: (Int, String) -> Unit, modifier: Modif
Column(modifier = Modifier.padding(20.dp)) {
Text(
text = stringResource(Res.string.notes),
- style = MaterialTheme.typography.titleMedium,
+ style = MaterialTheme.typography.titleMediumEmphasized,
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Bold,
)

diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailContent.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailContent.kt
index 46b5811271..62044a0e5f 100644
--- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailContent.kt
+++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailContent.kt
@@ -14,6 +14,8 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
+
package org.meshtastic.feature.node.detail
import androidx.compose.animation.Crossfade
@@ -24,7 +26,8 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
-import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.CircularWavyProgressIndicator
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -73,7 +76,7 @@ fun NodeDetailContent(
} else {
val loadingDescription = stringResource(Res.string.loading)
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
- CircularProgressIndicator(modifier = Modifier.semantics { contentDescription = loadingDescription })
+ CircularWavyProgressIndicator(modifier = Modifier.semantics { contentDescription = loadingDescription })
}
}
}

diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PowerMetrics.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PowerMetrics.kt
index 8920ee7257..577a8b612d 100644
--- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PowerMetrics.kt
+++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PowerMetrics.kt
@@ -42,7 +42,6 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -363,11 +362,7 @@ private fun hasChannelData(voltage: Float?, current: Float?): Boolean = voltage
@Composable
private fun PowerChannelColumn(titleRes: StringResource, voltage: Float, current: Float) {
Column {
- Text(
- text = stringResource(titleRes),
- style = TextStyle(fontWeight = FontWeight.Bold),
- fontSize = MaterialTheme.typography.labelLarge.fontSize,
- )
+ Text(text = stringResource(titleRes), style = MaterialTheme.typography.labelLarge, fontWeight = FontWeight.Bold)
MetricValueRow(color = PowerMetric.VOLTAGE.color, text = MetricFormatter.voltage(voltage))
MetricValueRow(color = PowerMetric.CURRENT.color, text = MetricFormatter.current(current))
}

diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/AboutScreen.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/AboutScreen.kt
index 751b5511da..6c1367b3af 100644
--- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/AboutScreen.kt
+++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/AboutScreen.kt
@@ -14,6 +14,8 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
+
package org.meshtastic.feature.settings
import androidx.compose.foundation.layout.Box
@@ -21,6 +23,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
@@ -112,7 +115,7 @@ private fun AboutHeader() {
Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 12.dp)) {
Text(
text = stringResource(Res.string.open_source_libraries),
- style = MaterialTheme.typography.titleMedium,
+ style = MaterialTheme.typography.titleMediumEmphasized,
color = MaterialTheme.colorScheme.primary,
)
Text(

diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/component/ExpressiveSection.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/component/ExpressiveSection.kt
index 4dc4913b60..9305ee6ba4 100644
--- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/component/ExpressiveSection.kt
+++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/component/ExpressiveSection.kt
@@ -14,6 +14,8 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
+
package org.meshtastic.feature.settings.component
import androidx.compose.foundation.layout.Arrangement
@@ -23,6 +25,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -43,7 +46,7 @@ fun ExpressiveSection(
Text(
text = title,
modifier = Modifier.padding(horizontal = 16.dp).fillMaxWidth(),
- style = MaterialTheme.typography.titleMedium,
+ style = MaterialTheme.typography.titleMediumEmphasized,
fontWeight = FontWeight.Bold,
color = titleColor,
)

diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/DebugFilters.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/DebugFilters.kt
index 3d879cbdeb..6dbe9f1a59 100644
--- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/DebugFilters.kt
+++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/DebugFilters.kt
@@ -43,7 +43,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.DpOffset
@@ -129,7 +128,8 @@ fun DebugPresetFilters(
Column(modifier = modifier) {
Text(
text = stringResource(Res.string.debug_filter_preset_title),
- style = TextStyle(fontWeight = FontWeight.Bold),
+ style = MaterialTheme.typography.bodyMedium,
+ fontWeight = FontWeight.Bold,
modifier = Modifier.padding(vertical = 4.dp),
)
FlowRow(
@@ -185,7 +185,8 @@ fun DebugFilterBar(
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = stringResource(Res.string.debug_filters),
- style = TextStyle(fontWeight = FontWeight.Bold),
+ style = MaterialTheme.typography.bodyMedium,
+ fontWeight = FontWeight.Bold,
)
Icon(
imageVector =
@@ -245,7 +246,8 @@ fun DebugActiveFilters(
) {
Text(
text = stringResource(Res.string.debug_active_filters),
- style = TextStyle(fontWeight = FontWeight.Bold),
+ style = MaterialTheme.typography.bodyMedium,
+ fontWeight = FontWeight.Bold,
)
TextButton(
onClick = {

diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/DebugSearch.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/DebugSearch.kt
index 30c86a52ec..782308fb1b 100644
--- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/DebugSearch.kt
+++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/DebugSearch.kt
@@ -39,10 +39,8 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.debug_default_search
@@ -73,7 +71,7 @@ fun DebugSearchNavigation(
Text(
text = "${searchState.currentMatchIndex + 1}/${searchState.allMatches.size}",
modifier = Modifier.padding(end = 4.dp),
- style = TextStyle(fontSize = 12.sp),
+ style = MaterialTheme.typography.labelSmall,
)
IconButton(onClick = onPreviousMatch, enabled = searchState.hasMatches, modifier = Modifier.size(32.dp)) {
Icon(

diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsScreen.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsScreen.kt
index 680114afc2..7ee3f603e3 100644
--- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsScreen.kt
+++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsScreen.kt
@@ -14,6 +14,8 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
+
package org.meshtastic.feature.settings.filter
import androidx.compose.foundation.layout.Arrangement
@@ -26,6 +28,7 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Card
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
@@ -121,7 +124,7 @@ private fun FilterEnableCard(enabled: Boolean, onToggle: (Boolean) -> Unit) {
verticalAlignment = Alignment.CenterVertically,
) {
Column(modifier = Modifier.weight(1f)) {
- Text(stringResource(Res.string.filter_enable), style = MaterialTheme.typography.titleMedium)
+ Text(stringResource(Res.string.filter_enable), style = MaterialTheme.typography.titleMediumEmphasized)
Text(
stringResource(Res.string.filter_enable_summary),
style = MaterialTheme.typography.bodySmall,
@@ -137,7 +140,7 @@ private fun FilterEnableCard(enabled: Boolean, onToggle: (Boolean) -> Unit) {
private fun FilterWordsInputCard(newWord: String, onNewWordChange: (String) -> Unit, onAddWord: () -> Unit) {
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp)) {
- Text(stringResource(Res.string.filter_words), style = MaterialTheme.typography.titleMedium)
+ Text(stringResource(Res.string.filter_words), style = MaterialTheme.typography.titleMediumEmphasized)
Text(
stringResource(Res.string.filter_words_summary),
style = MaterialTheme.typography.bodySmall,

diff --git a/feature/wifi-provision/src/commonMain/kotlin/org/meshtastic/feature/wifiprovision/ui/WifiProvisionScreen.kt b/feature/wifi-provision/src/commonMain/kotlin/org/meshtastic/feature/wifiprovision/ui/WifiProvisionScreen.kt
index e824ddadfe..0e89e4d9a6 100644
--- a/feature/wifi-provision/src/commonMain/kotlin/org/meshtastic/feature/wifiprovision/ui/WifiProvisionScreen.kt
+++ b/feature/wifi-provision/src/commonMain/kotlin/org/meshtastic/feature/wifiprovision/ui/WifiProvisionScreen.kt
@@ -14,6 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
@file:Suppress("TooManyFunctions", "LongMethod")
package org.meshtastic.feature.wifiprovision.ui
@@ -55,7 +56,7 @@ import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconToggleButton
-import androidx.compose.material3.LinearProgressIndicator
+import androidx.compose.material3.LinearWavyProgressIndicator
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.LoadingIndicator
@@ -200,7 +201,7 @@ fun WifiProvisionScreen(
Column(modifier = Modifier.padding(padding).fillMaxSize().animateContentSize()) {
// Indeterminate progress bar for active operations
if (uiState.phase.isLoading) {
- LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
+ LinearWavyProgressIndicator(modifier = Modifier.fillMaxWidth())
} else {
Spacer(Modifier.height(4.dp))
}

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotComposableAlert_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotComposableAlert_Dark_d19fbf1f_0.png
index 8a2f075a41..88a16d9e69 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotComposableAlert_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotComposableAlert_Dark_d19fbf1f_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotComposableAlert_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotComposableAlert_Light_b29dc7a7_0.png
index dfbcea3181..3beac10a49 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotComposableAlert_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotComposableAlert_Light_b29dc7a7_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotHtmlAlert_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotHtmlAlert_Dark_d19fbf1f_0.png
index ab77a693dd..7b1b4821b2 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotHtmlAlert_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotHtmlAlert_Dark_d19fbf1f_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotHtmlAlert_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotHtmlAlert_Light_b29dc7a7_0.png
index e10f03b4b1..db8b196475 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotHtmlAlert_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotHtmlAlert_Light_b29dc7a7_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotIconAlert_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotIconAlert_Dark_d19fbf1f_0.png
index 5c2efc58d6..4533f3b842 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotIconAlert_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotIconAlert_Dark_d19fbf1f_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotIconAlert_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotIconAlert_Light_b29dc7a7_0.png
index a0e4aa13b8..2dc83b0e70 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotIconAlert_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotIconAlert_Light_b29dc7a7_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotMultipleChoiceAlert_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotMultipleChoiceAlert_Dark_d19fbf1f_0.png
index 6a66b82108..aa3dcc7424 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotMultipleChoiceAlert_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotMultipleChoiceAlert_Dark_d19fbf1f_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotMultipleChoiceAlert_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotMultipleChoiceAlert_Light_b29dc7a7_0.png
index 7e54d473dd..d64d886374 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotMultipleChoiceAlert_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotMultipleChoiceAlert_Light_b29dc7a7_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotTextAlert_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotTextAlert_Dark_d19fbf1f_0.png
index a317c423f0..544022e019 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotTextAlert_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotTextAlert_Dark_d19fbf1f_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotTextAlert_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotTextAlert_Light_b29dc7a7_0.png
index 0fd4fa0505..69e4b0e638 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotTextAlert_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotTextAlert_Light_b29dc7a7_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotChannelItem_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotChannelItem_Dark_d19fbf1f_0.png
index 1bca5bdc47..1b4a8b4fca 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotChannelItem_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotChannelItem_Dark_d19fbf1f_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotChannelItem_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotChannelItem_Light_b29dc7a7_0.png
index 06d194c769..f7d7130169 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotChannelItem_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotChannelItem_Light_b29dc7a7_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotListItemDisabled_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotListItemDisabled_Dark_d19fbf1f_0.png
index 1d279597fe..9db0caa8a5 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotListItemDisabled_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotListItemDisabled_Dark_d19fbf1f_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotListItemDisabled_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotListItemDisabled_Light_b29dc7a7_0.png
index 1d279597fe..d8895a9a8a 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotListItemDisabled_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotListItemDisabled_Light_b29dc7a7_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotListItem_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotListItem_Dark_d19fbf1f_0.png
index 1d279597fe..ccae0c5473 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotListItem_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotListItem_Dark_d19fbf1f_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotListItem_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotListItem_Light_b29dc7a7_0.png
index 1d279597fe..622247d7a3 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotListItem_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotListItem_Light_b29dc7a7_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSwitchListItem_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSwitchListItem_Dark_d19fbf1f_0.png
index 77eb25dc52..f012b5cffc 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSwitchListItem_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSwitchListItem_Dark_d19fbf1f_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSwitchListItem_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSwitchListItem_Light_b29dc7a7_0.png
index 6498fa6b49..aca4a66fc9 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSwitchListItem_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSwitchListItem_Light_b29dc7a7_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotIAQScale_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotIAQScale_Dark_d19fbf1f_0.png
index 23a4a65589..589eeebbe8 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotIAQScale_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotIAQScale_Dark_d19fbf1f_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotIAQScale_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotIAQScale_Light_b29dc7a7_0.png
index 23a4a65589..589eeebbe8 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotIAQScale_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotIAQScale_Light_b29dc7a7_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotPositionPrecisionPreference_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotPositionPrecisionPreference_Dark_d19fbf1f_0.png
index e1f86afd4c..60e79e2b6f 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotPositionPrecisionPreference_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotPositionPrecisionPreference_Dark_d19fbf1f_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotPositionPrecisionPreference_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotPositionPrecisionPreference_Light_b29dc7a7_0.png
index 8eb0703bf3..296ae5cd21 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotPositionPrecisionPreference_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotPositionPrecisionPreference_Light_b29dc7a7_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotRegularPreference_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotRegularPreference_Dark_d19fbf1f_0.png
index d392e5abfb..ce33dd8ba8 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotRegularPreference_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotRegularPreference_Dark_d19fbf1f_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotRegularPreference_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotRegularPreference_Light_b29dc7a7_0.png
index 69b55f4832..6bac5a2bec 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotRegularPreference_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotRegularPreference_Light_b29dc7a7_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotSliderPreferenceDisabled_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotSliderPreferenceDisabled_Dark_d19fbf1f_0.png
index 3a9df05fc1..26f4682caa 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotSliderPreferenceDisabled_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotSliderPreferenceDisabled_Dark_d19fbf1f_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotSliderPreferenceDisabled_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotSliderPreferenceDisabled_Light_b29dc7a7_0.png
index d10163aab8..923e98b6dc 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotSliderPreferenceDisabled_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotSliderPreferenceDisabled_Light_b29dc7a7_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotSliderPreference_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotSliderPreference_Dark_d19fbf1f_0.png
index 1130fdcfd7..7179d81584 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotSliderPreference_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotSliderPreference_Dark_d19fbf1f_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotSliderPreference_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotSliderPreference_Light_b29dc7a7_0.png
index f05643897b..49ed1ba7ed 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotSliderPreference_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotSliderPreference_Light_b29dc7a7_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotSwitchPreference_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotSwitchPreference_Dark_d19fbf1f_0.png
index 0f2ab59761..7c0aa30fe5 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotSwitchPreference_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotSwitchPreference_Dark_d19fbf1f_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotSwitchPreference_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotSwitchPreference_Light_b29dc7a7_0.png
index c1b4557551..166d5cefc2 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotSwitchPreference_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotSwitchPreference_Light_b29dc7a7_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotTextDividerPreference_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotTextDividerPreference_Dark_d19fbf1f_0.png
index 4c45f42be8..9c007bd135 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotTextDividerPreference_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotTextDividerPreference_Dark_d19fbf1f_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotTextDividerPreference_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotTextDividerPreference_Light_b29dc7a7_0.png
index 01537fb615..cec1b87b27 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotTextDividerPreference_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotTextDividerPreference_Light_b29dc7a7_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotConnectingDeviceInfo_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotConnectingDeviceInfo_Dark_d19fbf1f_0.png
index 6425535d29..d563d182d0 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotConnectingDeviceInfo_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotConnectingDeviceInfo_Dark_d19fbf1f_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotConnectingDeviceInfo_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotConnectingDeviceInfo_Light_b29dc7a7_0.png
index 1fc10a657f..573d8b84d4 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotConnectingDeviceInfo_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotConnectingDeviceInfo_Light_b29dc7a7_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotDeviceListItem_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotDeviceListItem_Dark_d19fbf1f_0.png
index d212cbd299..4481d90e04 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotDeviceListItem_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotDeviceListItem_Dark_d19fbf1f_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotDeviceListItem_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotDeviceListItem_Light_b29dc7a7_0.png
index 6377595580..3c6f47d986 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotDeviceListItem_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotDeviceListItem_Light_b29dc7a7_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotDeviceSectionHeader_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotDeviceSectionHeader_Dark_d19fbf1f_0.png
index 313bda88bb..c62dcc3f79 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotDeviceSectionHeader_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotDeviceSectionHeader_Dark_d19fbf1f_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotDeviceSectionHeader_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotDeviceSectionHeader_Light_b29dc7a7_0.png
index b26931092b..db9f0c3ece 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotDeviceSectionHeader_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotDeviceSectionHeader_Light_b29dc7a7_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotEmptyStateContent_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotEmptyStateContent_Dark_d19fbf1f_0.png
index 86ab86bf35..32fab55bbb 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotEmptyStateContent_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotEmptyStateContent_Dark_d19fbf1f_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotEmptyStateContent_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotEmptyStateContent_Light_b29dc7a7_0.png
index 322ac11620..096d5690bd 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotEmptyStateContent_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotEmptyStateContent_Light_b29dc7a7_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareChecking_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareChecking_Dark_d19fbf1f_0.png
index 6cc3826f0c..eb0889444f 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareChecking_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareChecking_Dark_d19fbf1f_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareChecking_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareChecking_Light_b29dc7a7_0.png
index 6a60c726dd..a22d68b3b7 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareChecking_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareChecking_Light_b29dc7a7_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareDisclaimer_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareDisclaimer_Dark_d19fbf1f_0.png
index 53af1fb892..6735848b60 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareDisclaimer_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareDisclaimer_Dark_d19fbf1f_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareDisclaimer_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareDisclaimer_Light_b29dc7a7_0.png
index 322df1f5d5..346e865b1f 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareDisclaimer_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareDisclaimer_Light_b29dc7a7_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareError_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareError_Dark_d19fbf1f_0.png
index 911520c688..677486d806 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareError_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareError_Dark_d19fbf1f_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareError_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareError_Light_b29dc7a7_0.png
index 5ddb4d376d..30c34d13e2 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareError_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareError_Light_b29dc7a7_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareVerifying_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareVerifying_Dark_d19fbf1f_0.png
index 4a7c0d6e16..152cbb3413 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareVerifying_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareVerifying_Dark_d19fbf1f_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareVerifying_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareVerifying_Light_b29dc7a7_0.png
index 2795e38578..cf4807bc22 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareVerifying_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareVerifying_Light_b29dc7a7_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/IntroScreenshotTestsKt/ScreenshotWelcomeScreen_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/IntroScreenshotTestsKt/ScreenshotWelcomeScreen_Dark_d19fbf1f_0.png
index b45934b6fa..866a0517e4 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/IntroScreenshotTestsKt/ScreenshotWelcomeScreen_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/IntroScreenshotTestsKt/ScreenshotWelcomeScreen_Dark_d19fbf1f_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/IntroScreenshotTestsKt/ScreenshotWelcomeScreen_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/IntroScreenshotTestsKt/ScreenshotWelcomeScreen_Light_b29dc7a7_0.png
index effb2699c1..05bad8abb5 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/IntroScreenshotTestsKt/ScreenshotWelcomeScreen_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/IntroScreenshotTestsKt/ScreenshotWelcomeScreen_Light_b29dc7a7_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotEditQuickChatDialog_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotEditQuickChatDialog_Dark_d19fbf1f_0.png
index abe5295bfb..a5a4a0d5b3 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotEditQuickChatDialog_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotEditQuickChatDialog_Dark_d19fbf1f_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotEditQuickChatDialog_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotEditQuickChatDialog_Light_b29dc7a7_0.png
index 7fc971ce6e..0506b10e32 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotEditQuickChatDialog_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotEditQuickChatDialog_Light_b29dc7a7_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotQuickChatItem_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotQuickChatItem_Dark_d19fbf1f_0.png
index caa2c47e30..15e3e531b9 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotQuickChatItem_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotQuickChatItem_Dark_d19fbf1f_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotQuickChatItem_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotQuickChatItem_Light_b29dc7a7_0.png
index 9418710db3..3d2c8bf35f 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotQuickChatItem_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotQuickChatItem_Light_b29dc7a7_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotDeviceActionsRemote_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotDeviceActionsRemote_Dark_d19fbf1f_0.png
index 512e7a3381..2cadad8e28 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotDeviceActionsRemote_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotDeviceActionsRemote_Dark_d19fbf1f_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotDeviceActionsRemote_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotDeviceActionsRemote_Light_b29dc7a7_0.png
index dfeba52858..03050b9afa 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotDeviceActionsRemote_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotDeviceActionsRemote_Light_b29dc7a7_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentLoading_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentLoading_Dark_d19fbf1f_0.png
index 1c711b83f9..56a5ab0fd7 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentLoading_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentLoading_Dark_d19fbf1f_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentLoading_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentLoading_Light_b29dc7a7_0.png
index 539f900f7e..4c6a233594 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentLoading_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentLoading_Light_b29dc7a7_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentRemote_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentRemote_Dark_d19fbf1f_0.png
index 13d5b6023a..dba88945f3 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentRemote_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentRemote_Dark_d19fbf1f_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentRemote_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentRemote_Light_b29dc7a7_0.png
index cc571c3b1d..c02399c867 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentRemote_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentRemote_Light_b29dc7a7_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotPositionInlineContent_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotPositionInlineContent_Dark_d19fbf1f_0.png
index a89b64538f..77b5de0623 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotPositionInlineContent_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotPositionInlineContent_Dark_d19fbf1f_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotPositionInlineContent_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotPositionInlineContent_Light_b29dc7a7_0.png
index 5ba415233b..02546c7be5 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotPositionInlineContent_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotPositionInlineContent_Light_b29dc7a7_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotAppInfoSection_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotAppInfoSection_Dark_d19fbf1f_0.png
index dcd9c21410..ec724ed04d 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotAppInfoSection_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotAppInfoSection_Dark_d19fbf1f_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotAppInfoSection_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotAppInfoSection_Light_b29dc7a7_0.png
index ff78aa0353..c324cc2f25 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotAppInfoSection_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotAppInfoSection_Light_b29dc7a7_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotAppearanceSection_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotAppearanceSection_Dark_d19fbf1f_0.png
index 0967e1dace..a8d0eab527 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotAppearanceSection_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotAppearanceSection_Dark_d19fbf1f_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotAppearanceSection_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotAppearanceSection_Light_b29dc7a7_0.png
index aecd2b6208..ff3b997181 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotAppearanceSection_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotAppearanceSection_Light_b29dc7a7_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotPersistenceSection_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotPersistenceSection_Dark_d19fbf1f_0.png
index d619c670c0..e4a809e8f9 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotPersistenceSection_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotPersistenceSection_Dark_d19fbf1f_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotPersistenceSection_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotPersistenceSection_Light_b29dc7a7_0.png
index f2f5aac537..5daff81b02 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotPersistenceSection_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotPersistenceSection_Light_b29dc7a7_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotTakServerSectionDisabled_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotTakServerSectionDisabled_Dark_d19fbf1f_0.png
index f1948c77c2..f5310fc88d 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotTakServerSectionDisabled_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotTakServerSectionDisabled_Dark_d19fbf1f_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotTakServerSectionDisabled_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotTakServerSectionDisabled_Light_b29dc7a7_0.png
index 023b84a01e..34ea45c3ee 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotTakServerSectionDisabled_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotTakServerSectionDisabled_Light_b29dc7a7_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotTakServerSectionEnabled_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotTakServerSectionEnabled_Dark_d19fbf1f_0.png
index b6019533b7..3b6e876b71 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotTakServerSectionEnabled_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotTakServerSectionEnabled_Dark_d19fbf1f_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotTakServerSectionEnabled_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotTakServerSectionEnabled_Light_b29dc7a7_0.png
index 0d2cc98188..c6271875d5 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotTakServerSectionEnabled_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotTakServerSectionEnabled_Light_b29dc7a7_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotTakTestCardResults_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotTakTestCardResults_Dark_d19fbf1f_0.png
index 1e7bb644f4..124c1956ab 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotTakTestCardResults_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotTakTestCardResults_Dark_d19fbf1f_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotTakTestCardResults_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotTakTestCardResults_Light_b29dc7a7_0.png
index 25787438fa..8ac5d46040 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotTakTestCardResults_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotTakTestCardResults_Light_b29dc7a7_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotTakTestCardRunning_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotTakTestCardRunning_Dark_d19fbf1f_0.png
index 278a6fd198..06115f878d 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotTakTestCardRunning_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotTakTestCardRunning_Dark_d19fbf1f_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotTakTestCardRunning_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotTakTestCardRunning_Light_b29dc7a7_0.png
index 27dd257d0d..113b8c49e3 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotTakTestCardRunning_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotTakTestCardRunning_Light_b29dc7a7_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotConnectedFailed_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotConnectedFailed_Dark_d19fbf1f_0.png
index 16a07222b9..cd9db3ee6f 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotConnectedFailed_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotConnectedFailed_Dark_d19fbf1f_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotConnectedFailed_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotConnectedFailed_Light_b29dc7a7_0.png
index b0504a8745..e880274abe 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotConnectedFailed_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotConnectedFailed_Light_b29dc7a7_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotConnectedSuccess_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotConnectedSuccess_Dark_d19fbf1f_0.png
index e5cea75cf1..2f536f76c8 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotConnectedSuccess_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotConnectedSuccess_Dark_d19fbf1f_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotConnectedSuccess_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotConnectedSuccess_Light_b29dc7a7_0.png
index fe4b685507..c0c05a47e0 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotConnectedSuccess_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotConnectedSuccess_Light_b29dc7a7_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotConnectedWithNetworks_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotConnectedWithNetworks_Dark_d19fbf1f_0.png
index d2fe80311d..30450aad9a 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotConnectedWithNetworks_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotConnectedWithNetworks_Dark_d19fbf1f_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotConnectedWithNetworks_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotConnectedWithNetworks_Light_b29dc7a7_0.png
index b0d27ce323..6ca3bb2022 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotConnectedWithNetworks_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotConnectedWithNetworks_Light_b29dc7a7_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotDeviceFound_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotDeviceFound_Dark_d19fbf1f_0.png
index 77b2ba9810..a9ea459b7d 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotDeviceFound_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotDeviceFound_Dark_d19fbf1f_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotDeviceFound_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotDeviceFound_Light_b29dc7a7_0.png
index 6f499bd453..64256dbc38 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotDeviceFound_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotDeviceFound_Light_b29dc7a7_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotNetworkRow_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotNetworkRow_Dark_d19fbf1f_0.png
index 0a723868ae..2ac94f3bf9 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotNetworkRow_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotNetworkRow_Dark_d19fbf1f_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotNetworkRow_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotNetworkRow_Light_b29dc7a7_0.png
index 5fd1543200..47db7a6f57 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotNetworkRow_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotNetworkRow_Light_b29dc7a7_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotProvisionStatusCardSuccess_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotProvisionStatusCardSuccess_Dark_d19fbf1f_0.png
index 57ffb37861..10ccc79635 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotProvisionStatusCardSuccess_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotProvisionStatusCardSuccess_Dark_d19fbf1f_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotProvisionStatusCardSuccess_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotProvisionStatusCardSuccess_Light_b29dc7a7_0.png
index 187fe1e81e..eaff030f96 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotProvisionStatusCardSuccess_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotProvisionStatusCardSuccess_Light_b29dc7a7_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotScanningBle_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotScanningBle_Dark_d19fbf1f_0.png
index 454bd623f9..ac98eb3198 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotScanningBle_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotScanningBle_Dark_d19fbf1f_0.png differ

diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotScanningBle_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotScanningBle_Light_b29dc7a7_0.png
index 9e45f1cdb1..67df31f38b 100644
Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotScanningBle_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotScanningBle_Light_b29dc7a7_0.png differ

diff --git a/specs/20260513-160000-m3-expressive-adoption/checklists/requirements.md b/specs/20260513-160000-m3-expressive-adoption/checklists/requirements.md
new file mode 100644
index 0000000000..14293c70ab
--- /dev/null
+++ b/specs/20260513-160000-m3-expressive-adoption/checklists/requirements.md
@@ -0,0 +1,37 @@
+# Specification Quality Checklist: M3 Expressive Design System Adoption
+
+**Purpose**: Validate specification completeness and quality before proceeding to planning
+**Created**: 2026-05-13
+**Feature**: [spec.md](../spec.md)
+
+## Content Quality
+
+- [x] No implementation details (languages, frameworks, APIs)
+- [x] Focused on user value and business needs
+- [x] Written for non-technical stakeholders
+- [x] All mandatory sections completed
+
+## Requirement Completeness
+
+- [x] No [NEEDS CLARIFICATION] markers remain
+- [x] Requirements are testable and unambiguous
+- [x] Success criteria are measurable
+- [x] Success criteria are technology-agnostic (no implementation details)
+- [x] All acceptance scenarios are defined
+- [x] Edge cases are identified
+- [x] Scope is clearly bounded
+- [x] Dependencies and assumptions identified
+
+## Feature Readiness
+
+- [x] All functional requirements have clear acceptance criteria
+- [x] User scenarios cover primary flows
+- [x] Feature meets measurable outcomes defined in Success Criteria
+- [x] No implementation details leak into specification
+
+## Notes
+
+- Spec references `ExperimentalMaterial3ExpressiveApi` and `MaterialExpressiveTheme` as design system identifiers (not implementation guidance) — acceptable per M3E being the named design system
+- FR-012 mentions the `@OptIn` annotation — this is a boundary-level implementation constraint necessary for safety, not implementation detail
+- Typography variant names (e.g., `titleMediumEmphasized`) are M3 Expressive design token names, not code references
+- All success criteria measure user-observable outcomes (fps, tap count, render time, visual hierarchy) rather than code metrics

diff --git a/specs/20260513-160000-m3-expressive-adoption/contracts/expressive-typography-api.md b/specs/20260513-160000-m3-expressive-adoption/contracts/expressive-typography-api.md
new file mode 100644
index 0000000000..5bed7a5723
--- /dev/null
+++ b/specs/20260513-160000-m3-expressive-adoption/contracts/expressive-typography-api.md
@@ -0,0 +1,78 @@
+# Contract: Expressive Typography API
+
+**Module**: `core/ui` | **Source Set**: `commonMain`
+
+## Public API
+
+### Theme Typography Access
+
+```kotlin
+// Standard access (unchanged)
+MaterialTheme.typography.bodyLarge
+MaterialTheme.typography.titleMedium
+
+// Expressive emphasized access (requires @OptIn)
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+MaterialTheme.typography.displayLargeEmphasized
+MaterialTheme.typography.headlineMediumEmphasized
+MaterialTheme.typography.titleMediumEmphasized
+MaterialTheme.typography.bodyLargeEmphasized
+MaterialTheme.typography.labelLargeEmphasized
+```
+
+### Typography Definition (Type.kt)
+
+```kotlin
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+val AppTypography = Typography(
+ bodyLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp, // Design standards §5 minimum
+ lineHeight = 24.sp,
+ letterSpacing = 0.5.sp,
+ ),
+ bodyMedium = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp, // Design standards §5 minimum
+ lineHeight = 22.sp,
+ letterSpacing = 0.25.sp,
+ ),
+ // All other slots use M3 defaults — MaterialExpressiveTheme
+ // auto-generates emphasized variants from these base styles
+)
+```
+
+## Usage Guidelines
+
+| UI Context | Typography Slot | When Emphasized |
+|------------|----------------|-----------------|
+| Screen titles | `titleLarge` | Always use emphasized variant |
+| Section headers | `titleMedium` | Use emphasized variant |
+| Node names in list | `bodyLarge` | Use emphasized for own node |
+| Metric values (signal, battery) | `headlineSmall` | Primary metric value |
+| Metric labels | `bodyMedium` | Never emphasized |
+| Message body text | `bodyLarge` | Never emphasized |
+| Timestamps, metadata | `labelMedium` | Never emphasized |
+| Navigation labels | `labelLarge` | Active destination uses emphasized |
+| Button text | `labelLarge` | Always emphasized |
+| Dialog titles | `headlineSmall` | Always use emphasized variant |
+
+## Behavioral Contracts
+
+| Behavior | Specification |
+|----------|--------------|
+| Minimum body size | `bodyMedium.fontSize >= 16.sp` (hard requirement from design standards) |
+| Dynamic Type scaling | All sizes must scale with system `fontScale` up to 200% without clipping |
+| Pre-Android 12 | Graceful degradation: standard weight variants (400/500/700) instead of variable font intermediate weights |
+| Android 12+ | System Roboto Flex provides full variable weight expression |
+| Desktop/iOS | System default font family; same `sp` sizes; weight expression depends on platform font |
+
+## Migration Checklist (for each screen)
+
+1. Replace hardcoded `TextStyle` with `MaterialTheme.typography.*` reference
+2. Identify heading elements → use emphasized variant
+3. Verify no `fontSize` below 16.sp for body text
+4. Run screenshot test to capture new visual
+5. Verify TalkBack still reads correct content (no regression)

diff --git a/specs/20260513-160000-m3-expressive-adoption/contracts/swipe-to-reveal-api.md b/specs/20260513-160000-m3-expressive-adoption/contracts/swipe-to-reveal-api.md
new file mode 100644
index 0000000000..f7766c3b75
--- /dev/null
+++ b/specs/20260513-160000-m3-expressive-adoption/contracts/swipe-to-reveal-api.md
@@ -0,0 +1,128 @@
+# Contract: SwipeToRevealBox Component API
+
+**Module**: `core/ui` | **Source Set**: `commonMain`
+
+## Public API
+
+```kotlin
+/**
+ * A composable container that reveals action content behind the foreground
+ * when the user swipes horizontally. Supports bi-directional swipe with
+ * spring-physics animations.
+ *
+ * @param state The [SwipeToRevealState] controlling the drag position
+ * @param startContent Content revealed on right-swipe (e.g., "request position")
+ * @param endContent Content revealed on left-swipe (e.g., "mute")
+ * @param modifier Modifier applied to the container
+ * @param enableStartSwipe Whether right-swipe is enabled (default: true)
+ * @param enableEndSwipe Whether left-swipe is enabled (default: true)
+ * @param onActionTriggered Callback when a full-swipe threshold is crossed
+ * @param content The foreground content (the list item)
+ */
+@ExperimentalMaterial3ExpressiveApi
+@Composable
+fun SwipeToRevealBox(
+ state: SwipeToRevealState,
+ startContent: @Composable BoxScope.() -> Unit,
+ endContent: @Composable BoxScope.() -> Unit,
+ modifier: Modifier = Modifier,
+ enableStartSwipe: Boolean = true,
+ enableEndSwipe: Boolean = true,
+ onActionTriggered: (SwipeDirection) -> Unit = {},
+ content: @Composable BoxScope.() -> Unit,
+)
+
+/**
+ * Creates and remembers a [SwipeToRevealState].
+ *
+ * @param initialAnchor Starting anchor position (default: [SwipeAnchor.Start])
+ * @param confirmValueChange Optional callback to confirm anchor changes
+ */
+@Composable
+fun rememberSwipeToRevealState(
+ initialAnchor: SwipeAnchor = SwipeAnchor.Start,
+ confirmValueChange: (SwipeAnchor) -> Boolean = { true },
+): SwipeToRevealState
+
+enum class SwipeAnchor {
+ Start, // Resting (centered)
+ RevealEnd, // Left-swipe: end actions visible
+ RevealStart, // Right-swipe: start actions visible
+}
+
+enum class SwipeDirection {
+ StartToEnd, // Right-swipe
+ EndToStart, // Left-swipe
+}
+```
+
+## Usage Contract
+
+### Node List
+```kotlin
+SwipeToRevealBox(
+ state = rememberSwipeToRevealState(),
+ startContent = { RequestPositionAction(node) }, // Right-swipe
+ endContent = { MuteNodeAction(node) }, // Left-swipe
+ onActionTriggered = { direction ->
+ when (direction) {
+ SwipeDirection.StartToEnd -> viewModel.requestPosition(node)
+ SwipeDirection.EndToStart -> viewModel.muteNode(node)
+ }
+ }
+) {
+ NodeListItem(node)
+}
+```
+
+### Message List
+```kotlin
+SwipeToRevealBox(
+ state = rememberSwipeToRevealState(),
+ startContent = {},
+ endContent = { DeleteMessageAction(message) }, // Left-swipe only
+ enableStartSwipe = false,
+ onActionTriggered = { viewModel.deleteMessage(message) }
+) {
+ MessageListItem(message)
+}
+```
+
+## Behavioral Contracts
+
+| Behavior | Specification |
+|----------|--------------|
+| Reveal threshold | 56.dp horizontal drag reveals actions |
+| Full-swipe threshold | 160.dp or 50% item width (whichever is smaller) triggers `onActionTriggered` |
+| Fling velocity | ≥400.dp/s snaps to nearest anchor |
+| Return animation | Spring: stiffness=300f, dampingRatio=0.7f |
+| Reveal animation | Spring: stiffness=400f, dampingRatio=0.8f |
+| Disabled direction | Drag in disabled direction produces rubber-band resistance (max 16.dp) |
+| Accessibility | Swipe actions announced via semantics `customActions` for TalkBack |
+| Reduced motion | Instant snap (no spring animation) when `LocalReduceMotion.current` is true |
+
+## Discoverability Hint Contract
+
+```kotlin
+/**
+ * Modifier that applies a one-shot edge-peek animation to hint at
+ * swipe availability.
+ *
+ * @param enabled Whether the hint should animate (false = no-op)
+ * @param peekDistance The distance to peek (default: 24.dp)
+ * @param onHintShown Callback after hint animation completes
+ */
+fun Modifier.swipeHint(
+ enabled: Boolean,
+ peekDistance: Dp = 24.dp,
+ onHintShown: () -> Unit = {},
+): Modifier
+```
+
+| Behavior | Specification |
+|----------|--------------|
+| Trigger | First composition when `enabled = true` |
+| Animation | Offset item by `peekDistance` over 400ms spring, hold 1000ms, return over 400ms spring |
+| Frequency | Once per list screen visit per session (until permanent dismissal) |
+| Permanent dismissal | After first successful swipe-to-action, `hasCompletedSwipeAction` = true |
+| Reduced motion | Skip animation entirely; rely on a subtle visual indicator (trailing arrow icon) |

diff --git a/specs/20260513-160000-m3-expressive-adoption/data-model.md b/specs/20260513-160000-m3-expressive-adoption/data-model.md
new file mode 100644
index 0000000000..2d02836945
--- /dev/null
+++ b/specs/20260513-160000-m3-expressive-adoption/data-model.md
@@ -0,0 +1,120 @@
+# Data Model: M3 Expressive Design System Adoption
+
+**Date**: 2025-07-18 | **Branch**: `20260513-160000-m3-expressive-adoption`
+
+## Overview
+
+This feature is primarily a UI/styling upgrade with minimal data model impact. The only new persistent state is the swipe-action discoverability hint preference.
+
+## New Entities
+
+### SwipeHintPreference
+
+**Location**: `core/datastore` (user preferences)
+
+| Field | Type | Default | Description |
+|-------|------|---------|-------------|
+| `hasCompletedSwipeAction` | `Boolean` | `false` | Permanently dismisses swipe hint after first successful swipe action |
+
+**Storage**: DataStore `UserPreferences` proto or preferences file (existing infrastructure).
+
+**State Transitions**:
+```
+false → true (on first successful swipe-to-action completion)
+```
+One-way flag. Never resets to false (permanent dismissal).
+
+---
+
+### SwipeHintSessionState (transient)
+
+**Location**: In-memory, ViewModel-scoped
+
+| Field | Type | Default | Description |
+|-------|------|---------|-------------|
+| `hasShownHintThisSession` | `Boolean` | `false` | Prevents repeated hint animation within same process lifecycle |
+
+**Lifecycle**: Resets on process death. Within a session, hint shows once per list screen visit until `hasCompletedSwipeAction` becomes `true`.
+
+---
+
+## Modified Entities
+
+### AppTypography (Type.kt)
+
+**Current State**: `val AppTypography = Typography()` — empty default
+
+**Target State**: Full expressive typescale definition:
+
+| Property | Value Source |
+|----------|-------------|
+| `displayLarge` | Standard M3 default (auto-emphasized by expressive theme) |
+| `displayMedium` | Standard M3 default |
+| `displaySmall` | Standard M3 default |
+| `headlineLarge` | Standard M3 default |
+| `headlineMedium` | Standard M3 default |
+| `headlineSmall` | Standard M3 default |
+| `titleLarge` | Standard M3 default |
+| `titleMedium` | Standard M3 default |
+| `titleSmall` | Standard M3 default |
+| `bodyLarge` | `fontSize = 16.sp` minimum (design standards §5) |
+| `bodyMedium` | `fontSize = 16.sp` minimum (design standards §5) |
+| `bodySmall` | Standard M3 default |
+| `labelLarge` | Standard M3 default |
+| `labelMedium` | Standard M3 default |
+| `labelSmall` | Standard M3 default |
+
+**Note**: Emphasized variants (`displayLargeEmphasized`, `titleMediumEmphasized`, etc.) are auto-generated by `MaterialExpressiveTheme` based on the standard scale. No manual definition needed.
+
+---
+
+## Component State Models
+
+### SwipeToRevealState
+
+**Location**: `core/ui/component/SwipeToRevealBox.kt`
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `anchoredDraggableState` | `AnchoredDraggableState<SwipeAnchor>` | Manages drag position and anchoring |
+| `currentAnchor` | `SwipeAnchor` | Current snapped position |
+
+**SwipeAnchor Enum**:
+```kotlin
+enum class SwipeAnchor {
+ Start, // Resting position (0 offset)
+ RevealEnd, // Left-swipe reveals end actions
+ RevealStart, // Right-swipe reveals start actions
+}
+```
+
+---
+
+## Relationships
+
+```
+UserPreferences (DataStore)
+ └── hasCompletedSwipeAction: Boolean
+
+NodeListScreen
+ ├── uses SwipeToRevealBox (per item)
+ │ └── owns AnchoredDraggableState
+ └── reads hasCompletedSwipeAction (via ViewModel)
+
+MessageListScreen
+ ├── uses SwipeToRevealBox (per item)
+ │ └── owns AnchoredDraggableState
+ └── reads hasCompletedSwipeAction (via ViewModel)
+```
+
+---
+
+## Validation Rules
+
+| Rule | Constraint |
+|------|-----------|
+| `bodyLarge.fontSize` | `>= 16.sp` (design standards §5 minimum) |
+| `bodyMedium.fontSize` | `>= 16.sp` (design standards §5 minimum) |
+| Touch target minimum | `44.dp × 44.dp` on all interactive elements (design standards accessibility) |
+| Swipe threshold | Action reveals at ≥ 56.dp horizontal drag (comfortable thumb distance) |
+| Swipe velocity threshold | Fling detected at ≥ 400.dp/s (standard Material swipe velocity) |

diff --git a/specs/20260513-160000-m3-expressive-adoption/plan.md b/specs/20260513-160000-m3-expressive-adoption/plan.md
new file mode 100644
index 0000000000..5ed36c38a4
--- /dev/null
+++ b/specs/20260513-160000-m3-expressive-adoption/plan.md
@@ -0,0 +1,118 @@
+# Implementation Plan: M3 Expressive Design System Adoption
+
+**Branch**: `20260513-160000-m3-expressive-adoption` | **Date**: 2026-05-13 | **Spec**: `specs/20260513-160000-m3-expressive-adoption/spec.md`
+**Input**: Feature specification from `specs/20260513-160000-m3-expressive-adoption/spec.md`
+
+## Summary
+
+Adopt Material 3 Expressive design system fully across the Meshtastic Android app by extending the existing partial `MaterialExpressiveTheme` + `MotionScheme.expressive()` to all screens and components. Key deliverables: complete expressive typography with emphasized variants, spring-physics navigation indicators, upgraded core/ui component library (67+ composables), swipe-to-action on node/message lists (right=request position, left=mute), and accessibility focus ring support. Implementation uses only APIs available in CMP material3 1.11.0-alpha07 — no manual reimplementations, no feature flags, no font bundling.
+
+## Technical Context
+
+**Language/Version**: Kotlin 2.3+ / JDK 21 / KMP
+**Primary Dependencies**: CMP material3 `1.11.0-alpha07`, Compose Foundation (gestures), Compose Animation (spring)
+**Storage**: DataStore (single boolean preference for swipe hint dismissal)
+**Testing**: `./gradlew allTests` (KMP), `compose-screenshot 0.0.1-alpha14` (visual regression)
+**Target Platform**: Android API 24+, Compose Desktop, iOS (via commonMain)
+**Project Type**: KMP mobile/desktop app
+**Performance Goals**: 60fps animations on mid-range devices (Pixel 6a), cold-start ≤ 50ms regression
+**Constraints**: No APK size increase (no font bundling), no runtime feature flags, commonMain-only UI code
+**Scale/Scope**: 67+ core/ui components, 9 feature modules, ~50 screens
+
+## Constitution Check
+
+*GATE: ✅ PASSED (pre-design) — Re-checked post-design below.*
+
+- **I. Kotlin Multiplatform Core**: ✅ All changes are in `commonMain` source sets. Modules touched: `core/ui`, `core/datastore` (1 boolean pref), all 9 `feature/*` modules. No new `androidMain` code. The only existing `androidMain` code (DynamicColorScheme.kt) is unchanged.
+
+- **II. Zero Lint Tolerance**: ✅ Verification commands:
+ ```bash
+ ./gradlew spotlessApply spotlessCheck detekt
+ # Per-module: :core:ui:detekt :feature:node:detekt etc.
+ ```
+ All touched modules will pass `spotlessCheck` + `detekt` before push.
+
+- **III. Compose Multiplatform UI**: ✅ All UI uses Compose Multiplatform patterns. Navigation remains `MeshtasticNavDisplay` + `NavigationBackHandler`. No floats displayed (typography/animation values are internal). New `SwipeToRevealBox` composable uses only Compose Foundation APIs available in CMP.
+
+- **IV. Privacy First**: ✅ No PII, location data, or keys logged. No new network calls. `core/proto` untouched. The only new preference (`hasCompletedSwipeAction`) stores no sensitive data.
+
+- **V. Design Standards Compliance**: ✅ Verified against design standards v1.4:
+ - Typography: `bodyMedium/bodyLarge >= 16sp` (§5 minimum)
+ - Color: No palette changes; M3 role mapping (§8) unchanged
+ - Accessibility: 4.5:1 contrast maintained, 44dp touch targets, 200% Dynamic Type
+ - Cross-platform spec: N/A — M3 Expressive is platform-specific styling layer. The `meshtastic/design/features/` directory does not exist. Justification: this is an implementation-layer styling upgrade, not a UX behavior change.
+
+- **VI. Verify Before Push**: ✅ Commands:
+ ```bash
+ ./gradlew spotlessApply detekt assembleDebug test allTests
+ # Post-push:
+ gh pr checks <PR> || gh run list --branch 20260513-160000-m3-expressive-adoption --limit 5
+ ```
+
+### Post-Design Re-Check
+
+✅ All six principles remain satisfied after Phase 1 design:
+- New `SwipeToRevealBox` is in `commonMain` (Principle I ✅)
+- No lint exceptions introduced (Principle II ✅)
+- Uses CMP Foundation gestures, not Android-specific (Principle III ✅)
+- DataStore pref `hasCompletedSwipeAction` is non-sensitive (Principle IV ✅)
+- Typography/component changes maintain design standards compliance (Principle V ✅)
+- Build commands documented above (Principle VI ✅)
+
+## Project Structure
+
+### Documentation (this feature)
+
+```text
+specs/20260513-160000-m3-expressive-adoption/
+├── plan.md # This file
+├── research.md # Phase 0: technology decisions
+├── data-model.md # Phase 1: entities and state models
+├── quickstart.md # Phase 1: developer onboarding
+├── contracts/
+│ ├── swipe-to-reveal-api.md # SwipeToRevealBox public API contract
+│ └── expressive-typography-api.md # Typography usage guidelines
+└── tasks.md # Phase 2: implementation tasks (pending)
+```
+
+### Source Code (repository root)
+
+```text
+core/
+├── ui/src/commonMain/.../theme/
+│ ├── Theme.kt # MaterialExpressiveTheme (exists, extend)
+│ ├── Type.kt # AppTypography (exists, upgrade)
+│ └── Color.kt # Color tokens (unchanged)
+├── ui/src/commonMain/.../component/
+│ ├── SwipeToRevealBox.kt # NEW: swipe-to-action container
+│ ├── MeshtasticNavigationSuite.kt # MODIFY: expressive indicators
+│ ├── MainAppBar.kt # MODIFY: expressive styling
+│ ├── MenuFAB.kt # MODIFY: tooltip + shape
+│ ├── SliderPreference.kt # MODIFY: spring animation
+│ ├── AlertDialogs.kt # MODIFY: expressive shape/motion
+│ └── [63 other components] # MODIFY: typography references
+├── datastore/src/commonMain/...
+│ └── UserPreferences # MODIFY: add hasCompletedSwipeAction
+
+feature/
+├── node/src/commonMain/.../list/
+│ └── NodeListScreen.kt # MODIFY: integrate SwipeToRevealBox
+├── messaging/src/commonMain/...
+│ └── [message list screen] # MODIFY: integrate SwipeToRevealBox
+├── firmware/src/commonMain/... # MODIFY: wavy progress consistency
+├── connections/src/commonMain/... # MODIFY: FAB tooltip labels
+└── [5 other modules] # MODIFY: typography + component updates
+
+screenshot-tests/
+└── src/screenshotTest/... # MODIFY: update reference images
+```
+
+**Structure Decision**: Existing KMP multi-module architecture. No new modules created. One new file (`SwipeToRevealBox.kt`) in `core/ui/component/`. One new DataStore field. All other work is modifications to existing files.
+
+## Complexity Tracking
+
+> No Constitution violations. No complexity exceptions needed.
+
+| Violation | Why Needed | Simpler Alternative Rejected Because |
+|-----------|------------|-------------------------------------|
+| — | — | — |

diff --git a/specs/20260513-160000-m3-expressive-adoption/quickstart.md b/specs/20260513-160000-m3-expressive-adoption/quickstart.md
new file mode 100644
index 0000000000..79006b40a3
--- /dev/null
+++ b/specs/20260513-160000-m3-expressive-adoption/quickstart.md
@@ -0,0 +1,109 @@
+# Quickstart: M3 Expressive Adoption
+
+**Branch**: `20260513-160000-m3-expressive-adoption`
+
+## Prerequisites
+
+```bash
+# Standard bootstrap (if not already done)
+git submodule update --init
+[ -f local.properties ] || cp secrets.defaults.properties local.properties
+
+# Checkout the feature branch
+git checkout 20260513-160000-m3-expressive-adoption
+```
+
+## Key Files to Know
+
+| File | Purpose |
+|------|---------|
+| `core/ui/src/commonMain/.../theme/Theme.kt` | Root `AppTheme` — already uses `MaterialExpressiveTheme` |
+| `core/ui/src/commonMain/.../theme/Type.kt` | Typography definition — upgrade target |
+| `core/ui/src/commonMain/.../component/` | 67+ shared components — upgrade targets |
+| `feature/node/src/commonMain/.../list/NodeListScreen.kt` | Node list — swipe-to-action target |
+| `feature/messaging/src/commonMain/...` | Message list — swipe-to-action target |
+| `screenshot-tests/` | Visual regression tests |
+
+## Build & Verify
+
+```bash
+# Full verification (run after every change)
+./gradlew spotlessApply detekt assembleDebug test allTests
+
+# Update screenshot references after visual changes
+./gradlew :screenshot-tests:updateScreenshotTests
+
+# Single module test (e.g., after changing core/ui)
+./gradlew :core:ui:allTests
+```
+
+## Implementation Order
+
+1. **Typography** (Type.kt) — Foundation for all screens
+2. **Navigation** (MeshtasticNavigationSuite.kt) — Highest-traffic interaction
+3. **Core components** (MainAppBar, buttons, dialogs) — Shared across all features
+4. **FABs & progress** (MenuFAB, progress indicators) — Already partially expressive
+5. **Swipe-to-action** (new SwipeToRevealBox) — New component + integration
+6. **Feature module rollout** — Apply typography + component updates per module
+7. **Accessibility** — Focus rings, reduced-motion verification
+
+## Working with Expressive APIs
+
+```kotlin
+// Always annotate at function level in feature modules
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@Composable
+fun MyScreen() {
+ // Access emphasized typography
+ Text(
+ text = "Heading",
+ style = MaterialTheme.typography.titleMediumEmphasized,
+ )
+}
+```
+
+## Swipe-to-Action Pattern
+
+```kotlin
+// In node list item
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@Composable
+fun NodeListItemWithSwipe(node: NodeEntity, viewModel: NodeListViewModel) {
+ val state = rememberSwipeToRevealState()
+
+ SwipeToRevealBox(
+ state = state,
+ startContent = { RequestPositionAction() },
+ endContent = { MuteNodeAction() },
+ onActionTriggered = { direction ->
+ when (direction) {
+ SwipeDirection.StartToEnd -> viewModel.requestPosition(node)
+ SwipeDirection.EndToStart -> viewModel.muteNode(node)
+ }
+ },
+ ) {
+ NodeListItem(node)
+ }
+}
+```
+
+## Key Constraints (from clarification)
+
+| Constraint | Implication |
+|-----------|-------------|
+| No feature flags | Direct replacement — each PR is atomic and revertible via git |
+| No font bundling | Pre-Android 12 gets standard weight fallback; no APK size increase |
+| No manual reimplementations | If CMP material3 doesn't provide it, skip it entirely |
+| Swipe actions | Right = request position, Left = mute (node list) |
+| Discoverability hint | Per-session edge-peek until first successful swipe |
+
+## Commit Strategy
+
+One commit per user story on a single branch:
+1. Commit: Setup + Foundational (theme, typography, SwipeToRevealBox)
+2. Commit: Navigation expressive indicators (US1)
+3. Commit: Typography rollout across feature modules (US2)
+4. Commit: Core component upgrades (US3)
+5. Commit: SwipeToRevealBox + node/message list integration (US4)
+6. Commit: Accessibility + focus rings (US5)
+7. Commit: Screenshot test updates + polish (cross-cutting)

diff --git a/specs/20260513-160000-m3-expressive-adoption/research.md b/specs/20260513-160000-m3-expressive-adoption/research.md
new file mode 100644
index 0000000000..7808558ffc
--- /dev/null
+++ b/specs/20260513-160000-m3-expressive-adoption/research.md
@@ -0,0 +1,132 @@
+# Research: M3 Expressive Design System Adoption
+
+**Date**: 2025-07-18 | **Branch**: `20260513-160000-m3-expressive-adoption`
+
+## Research Questions & Findings
+
+### RQ-1: What expressive typography APIs does CMP material3 1.11.0-alpha07 provide?
+
+**Decision**: Use `MaterialTheme.typography` emphasized extension properties available in `ExperimentalMaterial3ExpressiveApi`.
+
+**Rationale**: CMP material3 1.11.0-alpha07 (backed by Jetpack Compose Material3 ~1.5.0-alpha) exposes `Typography` with emphasized variants accessible via `MaterialTheme.typography.displayLargeEmphasized`, `headlineMediumEmphasized`, `titleMediumEmphasized`, `bodyLargeEmphasized`, and `labelLargeEmphasized`. These are extension properties gated behind `@ExperimentalMaterial3ExpressiveApi`. The Typography constructor itself does not accept emphasized parameters directly — instead, the expressive theme auto-generates them based on the standard typescale using bolder font weights and slightly adjusted letter spacing.
+
+**Alternatives Considered**:
+- Manually constructing emphasized `TextStyle` variants → Rejected per constraint: no manual reimplementations of CMP APIs
+- Bundling Roboto Flex variable font → Rejected per constraint: accept degradation pre-Android 12
+
+**Pre-Android 12 Behavior**: Without Roboto Flex, the system falls back to standard Roboto weight variants (400/500/700). Emphasized styles degrade to the nearest available weight. No visual breakage, just less granular weight expression.
+
+---
+
+### RQ-2: What expressive component variants are available in CMP material3 1.11.0-alpha07?
+
+**Decision**: Use only officially provided expressive APIs; defer unavailable ones.
+
+**Available (confirmed in 1.11.0-alpha07)**:
+| Component | API | Notes |
+|-----------|-----|-------|
+| Theme | `MaterialExpressiveTheme` | ✅ Already in use |
+| Motion | `MotionScheme.expressive()` | ✅ Already in use |
+| FAB | `FloatingActionButtonDefaults.shape` / morphing | ✅ Expressive shapes available |
+| Progress | `CircularWavyProgressIndicator`, `LinearWavyProgressIndicator` | ✅ Wavy variants available |
+| Navigation Bar | `NavigationBarItemDefaults` expressive pill indicator | ✅ Available |
+| Navigation Rail | `NavigationRailItemDefaults` expressive pill indicator | ✅ Available |
+| Button | `ButtonDefaults` with expressive shapes | ✅ Round shapes available |
+| Slider | No explicit expressive variant | ⚠️ Use standard Slider with spring animation manually |
+| SwipeToDismissBox | `SwipeToDismissBoxState`, `SwipeToDismissBox` | ✅ Foundation API available |
+| Tooltip | `PlainTooltip`, `RichTooltip`, `TooltipBox` | ✅ Available |
+| Top App Bar | `TopAppBarDefaults` with expressive styling | ✅ Available |
+
+**Not Available (deferred per NFR-007)**:
+| Component | Status |
+|-----------|--------|
+| Expressive focus rings (animated) | Not in CMP material3 yet — use `Modifier.focusable()` with custom indication |
+| Shape morphing on press for FABs | Partial — `InteractionSource`-based shape is experimental |
+| Expressive Slider | No distinct expressive variant; use spring animateFloatAsState for thumb |
+
+**Rationale**: Per spec NFR-007 and clarification, we only use what's officially available. Animated focus rings and shape-morph-on-press are deferred until CMP ships them. Standard focus indication + spring-animated state changes provide acceptable intermediate UX.
+
+---
+
+### RQ-3: How to implement swipe-to-action on Compose Multiplatform lists?
+
+**Decision**: Use Compose Foundation `AnchoredDraggableState` + custom `SwipeToRevealBox` composable in `core/ui`.
+
+**Rationale**: `SwipeToDismissBox` from Material3 is designed for dismiss (delete) semantics. For reveal-actions (swipe to show action buttons without dismissing), the recommended pattern is:
+1. Use `AnchoredDraggableState` from `androidx.compose.foundation.gestures`
+2. Create anchors at rest (0), partial reveal (action width), and optionally full-swipe (action + execute)
+3. Render action content behind the foreground item using `Box` with offset
+
+This approach:
+- Works in `commonMain` (no platform-specific code needed)
+- Allows bi-directional swipe (left = mute, right = request position)
+- Supports spring animations via `AnchoredDraggableState.animateTo()` with spring spec
+- Is composable and reusable across node list and message list
+
+**Alternatives Considered**:
+- `SwipeToDismissBox` → Only supports dismiss semantics, not reveal-and-hold
+- Third-party swipe library → Violates principle of minimal dependencies; foundation API is sufficient
+- Custom `Modifier.pointerInput` → Lower-level; `AnchoredDraggable` handles fling velocity, snapping, and state persistence
+
+---
+
+### RQ-4: How should the swipe discoverability hint work?
+
+**Decision**: Animate a 24dp edge-peek on the first visible list item using `LaunchedEffect` with a spring, controlled by a DataStore boolean `hasCompletedSwipeAction`.
+
+**Rationale**: The spec requires "hint repeats each session until user completes a successful swipe action." This means:
+- Store `hasCompletedSwipeAction: Boolean` in user preferences (DataStore)
+- On first composition of node/message list screen, if `!hasCompletedSwipeAction`, run a one-shot animation that offsets the first item 24dp to reveal a sliver of the action background
+- After 1.0 seconds hold, spring back to rest position (400ms return)
+- Once the user successfully completes any swipe action, set `hasCompletedSwipeAction = true` and never show hint again
+
+**Session vs. Permanent**: Per clarification, hint "repeats each session." Interpretation: a "session" = app process lifecycle. Use an in-memory flag (ViewModel-scoped `MutableStateFlow<Boolean>`) that resets on process death. DataStore persists the "has ever swiped" permanent dismissal. The per-session hint shows if the permanent flag is false.
+
+---
+
+### RQ-5: How to apply spring-physics navigation indicator animations?
+
+**Decision**: The expressive `MotionScheme` already applies spring physics to navigation indicator movement when using `NavigationBar`/`NavigationRail` with `MaterialExpressiveTheme`. No additional configuration needed beyond ensuring the theme wraps all navigation composables.
+
+**Rationale**: `MotionScheme.expressive()` overrides the default animation specs for all Material3 component internal animations. The navigation indicator (pill shape) animation between items uses the motion scheme's `defaultSpatialSpec` which is a spring with `stiffness = 300f` and `dampingRatio = 0.7f`. This is automatic when the composable is inside `MaterialExpressiveTheme`.
+
+**Verification**: Can be confirmed by running the app in Layout Inspector and observing the indicator transition duration/curve. Should complete in ~250-350ms with slight overshoot.
+
+---
+
+### RQ-6: How to handle `@OptIn(ExperimentalMaterial3ExpressiveApi::class)` at scale?
+
+**Decision**: Apply file-level `@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)` in `core/ui/theme/Theme.kt` and `core/ui/theme/Type.kt`. For feature modules, use function-level `@OptIn` only on composables that directly call expressive APIs.
+
+**Rationale**:
+- File-level in theme files is acceptable because the entire theme is fundamentally expressive
+- Function-level in feature modules maintains visibility of which specific composables depend on experimental APIs
+- This enables easy grep/search for expressive API usage when evaluating migration path as APIs stabilize
+- Detekt/lint won't flag OptIn propagation issues with this approach
+
+---
+
+### RQ-7: Screenshot test strategy for visual regression during migration?
+
+**Decision**: Update existing screenshot tests incrementally per user story. Each PR that changes component appearance must update corresponding reference images.
+
+**Rationale**: The project uses `compose-screenshot 0.0.1-alpha14` with multi-variant test setup. Strategy:
+1. Before each component change, confirm current screenshot tests pass (baseline)
+2. After applying expressive styling, run `./gradlew updateScreenshotTests` to capture new references
+3. Include updated reference PNGs in the same PR as the styling change
+4. Review visual diff in PR to confirm intentional changes only
+
+This is straightforward incremental migration — no special infrastructure needed.
+
+---
+
+### RQ-8: Cross-platform spec requirement from Constitution §V?
+
+**Decision**: Mark as N/A — M3 Expressive is platform-specific (Android/Compose).
+
+**Rationale**: Per the spec's `Cross-Platform Spec` field: "N/A — platform-specific only. M3 Expressive APIs are Android/Compose-specific; desktop and iOS targets will receive equivalent improvements via CMP material3 as those APIs stabilize." Additionally, the `meshtastic/design` repo has no `features/` directory (404) and no mention of "expressive" anywhere. The design standards v1.4 do not prescribe animation/motion guidance, leaving this to platform implementations.
+
+Constitution §V is satisfied because:
+1. The feature does not change cross-platform behavior semantics
+2. It's an implementation-layer styling upgrade, not a UX flow change
+3. Desktop/iOS get the styling automatically through `commonMain` when CMP stabilizes the APIs

diff --git a/specs/20260513-160000-m3-expressive-adoption/spec.md b/specs/20260513-160000-m3-expressive-adoption/spec.md
new file mode 100644
index 0000000000..ef73f1eff0
--- /dev/null
+++ b/specs/20260513-160000-m3-expressive-adoption/spec.md
@@ -0,0 +1,136 @@
+# Feature Specification: M3 Expressive Design System Adoption
+
+**Feature Branch**: `20260513-160000-m3-expressive-adoption`
+**Created**: 2026-05-13
+**Status**: Implementation Complete
+**Input**: User description: "Adopt Material 3 Expressive design system across the Meshtastic Android app UI, leveraging new typography, animations, component styles, and interaction patterns to create a more dynamic and accessible user experience."
+**Cross-Platform Spec**: N/A — platform-specific only. M3 Expressive APIs are Android/Compose-specific; desktop and iOS targets will receive equivalent improvements via CMP material3 as those APIs stabilize.
+
+## Summary
+
+Adopt the Material 3 Expressive design system throughout the Meshtastic Android app by replacing custom UI implementations with native M3 library components from `compose-multiplatform-material3:1.11.0-alpha07`. The guiding principle is **zero self-rolled components** — only native M3 APIs are used. Custom wrappers that existed in `core/ui` have been migrated to thin delegating layers over native M3 components, or deleted entirely when they were dead code.
+
+## Goals
+
+1. **Unified expressive typography**: Replace hardcoded `TextStyle` references with M3 Expressive typescale including emphasized variants (`titleMediumEmphasized`, `headlineSmallEmphasized`, etc.) auto-generated by `MaterialExpressiveTheme`
+2. **Native M3 component adoption**: Migrate all custom component wrappers to delegate to native M3 interactive ListItem, ModalBottomSheet, OutlinedIconToggleButton, DropdownMenuGroup, CircularWavyProgressIndicator, etc.
+3. **Dead code elimination**: Remove unused custom components (ClickableTextField, TimeTickWithLifecycle, BottomSheetDialog) that were superseded or never called
+4. **Proper M3 disabled/color delegation**: Let M3 handle disabled content colors natively through its token system instead of manual alpha overrides
+5. **Process-death state preservation**: Maintain CurrentTabSaver for active tab persistence across process death
+
+## Non-Goals
+
+- Self-rolling components not available in CMP material3 (e.g., SwipeToRevealBox, animated focus rings, reduce-motion locals) — **deferred until native APIs ship**
+- Migrating all ~176 `HorizontalDivider` usages (separate follow-up PR)
+- Migrating remaining legacy `CircularProgressIndicator` sites (IndoorAirQuality, LoadingOverlay, TAKConfigItemList) — low-priority follow-up
+- Changing the app's color palette or brand identity
+- Introducing new features or screens — this is a design-system upgrade of existing UI
+- Supporting M3 Expressive on desktop/iOS targets immediately
+
+## What Was Implemented
+
+### Native M3 Components Adopted
+
+| Custom/Old | Native M3 Replacement | Location |
+|-----------|----------------------|----------|
+| Custom `ListItem` wrapper (clickable Column) | M3 interactive `ListItem(onClick=...)` | `core/ui/component/ListItem.kt` |
+| Custom `RegularPreference` (Column + clickable) | M3 interactive `ListItem` with slots | `core/ui/component/RegularPreference.kt` |
+| Custom `SwitchPreference` (toggleable ListItem) | M3 toggleable `ListItem(checked=..., onCheckedChange=...)` | `core/ui/component/SwitchPreference.kt` |
+| `IconToggleButton` | `OutlinedIconToggleButton` | `feature/node/component/DeviceActions.kt` |
+| `BottomSheetDialog` (custom) | `ModalBottomSheet` | `core/ui/emoji/EmojiPickerDialog.kt`, `feature/messaging/component/Reaction.kt` |
+| `CircularProgressIndicator` (in SwitchPref) | `CircularWavyProgressIndicator` | `core/ui/component/SwitchPreference.kt` |
+| Manual disabled color logic | M3 native disabled token delegation (`Color.Unspecified`) | All ListItem wrappers |
+| Hardcoded `TextStyle(...)` | `MaterialTheme.typography.*Emphasized` | All feature modules |
+
+### Dead Code Removed
+
+| Component | Reason |
+|-----------|--------|
+| `ClickableTextField` | Zero callers — superseded by ExposedDropdownMenuBox |
+| `TimeTickWithLifecycle` (expect + 3 actuals) | Zero callers — lifecycle tick no longer needed |
+| `BottomSheetDialog` | Zero callers after ModalBottomSheet migration |
+
+### Key Design Decisions
+
+1. **RectangleShape for resting ListItem**: M3 interactive ListItem defaults to `CornerExtraSmall` (4dp rounded) which clashed with `HorizontalDivider` between items. Override `ListItemDefaults.shapes(shape = RectangleShape)` preserves press/hover/focus shape animations while keeping divider compatibility.
+
+2. **Color.Unspecified over LocalContentColor.current**: Passing `Color.Unspecified` to Text/Icon in M3 slots lets M3 apply its own content colors including disabled states. Explicit colors override M3's provision.
+
+3. **icon() helper**: Resolves `Color.Unspecified` → `LocalContentColor.current` at render time so M3 disabled content color flows through while explicit colors (like error red) still work.
+
+4. **CurrentTabSaver**: `rememberSaveable` with ordinal-based saver persists the active navigation tab across process death. Was accidentally dropped during Route graph refactoring and restored.
+
+5. **ModalBottomSheet with skipPartiallyExpanded**: Both EmojiPickerDialog and ReactionDialog use `skipPartiallyExpanded = true` to show full content immediately rather than partial peek.
+
+## Architecture
+
+### Key Components
+
+| Component | Module / File | Purpose |
+|-----------|---------------|---------|
+| `AppTheme` | `core/ui/theme/Theme.kt` | Root theme using `MaterialExpressiveTheme` with full expressive typography |
+| `AppTypography` | `core/ui/theme/Type.kt` | Custom typescale with 16sp body minimum; emphasized variants auto-generated |
+| `BasicListItem` | `core/ui/component/ListItem.kt` | Thin wrapper over M3 interactive ListItem with RectangleShape and Color.Unspecified defaults |
+| `SwitchPreference` | `core/ui/component/SwitchPreference.kt` | Thin wrapper over M3 toggleable ListItem |
+| `RegularPreference` | `core/ui/component/RegularPreference.kt` | Thin wrapper over M3 interactive ListItem |
+| `MeshtasticDialog` | `core/ui/component/AlertDialogs.kt` | Uses expressive shape (28dp) and emphasized typography |
+| `MainAppBar` | `core/ui/component/MainAppBar.kt` | Uses `titleLargeEmphasized` typography |
+| `MultiBackstack` | `core/navigation/MultiBackstack.kt` | Tab state persisted via CurrentTabSaver |
+
+## Requirements *(final)*
+
+### Functional Requirements
+
+- **FR-001**: ✅ The app theme uses `MaterialExpressiveTheme` with expressive `MotionScheme` on all screens
+- **FR-002**: ✅ The typography system uses emphasized variants auto-generated by MaterialExpressiveTheme with 16sp body minimum
+- **FR-005**: ✅ SwitchPreference and FirmwareUpdateScreen use `CircularWavyProgressIndicator` for indeterminate states
+- **FR-008**: ✅ Primary action buttons use expressive button styling
+- **FR-011**: ✅ The top app bar uses expressive styling with emphasized typography
+- **FR-012**: ✅ Expressive APIs wrapped with `@OptIn(ExperimentalMaterial3ExpressiveApi::class)` at file level
+
+### Deferred Requirements (awaiting native CMP APIs)
+
+- **FR-003**: Navigation spring-physics indicators — already provided by MotionScheme.expressive(), no additional work needed
+- **FR-004**: FAB tooltip labels — available via PlainTooltip (already in MenuFAB)
+- **FR-006**: Expressive slider styling — deferred (no native API beyond current)
+- **FR-007**: Swipe-to-action gestures — **REVERTED** (no native SwipeToRevealBox in M3; self-rolled version violated zero-custom-component constraint)
+- **FR-009**: Animated focus rings — **REVERTED** (no native M3 focus indication API; custom implementation violated constraint)
+- **FR-010**: Reduce-motion local — **REVERTED** (M3's MotionScheme already respects system animator duration scale natively)
+
+### Non-Functional Requirements
+
+- **NFR-004**: ✅ All expressive API usage confined to `commonMain` source set
+- **NFR-005**: ✅ Screenshot tests updated for new component appearances
+- **NFR-006**: ✅ Direct permanent replacement with no feature flag
+- **NFR-007**: ✅ No manual reimplementations of APIs not yet available in CMP material3
+
+## Source-Set Impact
+
+| Source Set | Impact | Justification |
+|-----------|--------|---------------|
+| `commonMain` | Modified: theme, ListItem, SwitchPreference, RegularPreference, AlertDialogs, MainAppBar, EmojiPickerDialog, ChannelItem; Deleted: ClickableTextField, TimeTickWithLifecycle, BottomSheetDialog | All UI logic lives in commonMain |
+| `androidMain` | Deleted: TimeTickWithLifecycle actual | Dead code removal |
+| `jvmMain` | Deleted: TimeTickWithLifecycle actual | Dead code removal |
+| `iosMain` | Modified: NoopStubs.kt (removed TimeTickWithLifecycle actual) | Dead code removal |
+
+## Success Criteria *(final)*
+
+- **SC-001**: ✅ All screens render using `MaterialExpressiveTheme`
+- **SC-004**: ✅ Typography hierarchy renders with emphasized variants and distinct weight/size levels
+- **SC-008**: ✅ Zero accessibility regressions — existing TalkBack flows unaffected
+
+## Clarifications
+
+### Session 2026-05-18
+
+- Q: Should we use self-rolled M3 components (SwipeToRevealBox, focus indication, reduce motion) or only native library APIs? → A: **Only native library APIs.** All self-rolled implementations were reverted. Zero custom components policy.
+- Q: Should dividers between ListItems be removed now that M3 uses rounded shapes? → A: Keep dividers + use RectangleShape for resting state. Divider removal is a separate focused PR (~176 usages across 40 files).
+- Q: Should we use `OutlinedIconToggleButton` or regular `IconToggleButton` for favorite? → A: `OutlinedIconToggleButton` for clearer affordance.
+- Q: Should RegularPreference manually compute disabled colors? → A: No — delegate to M3's native disabled token system via `Color.Unspecified`.
+
+## Assumptions
+
+- CMP material3 library version 1.11.0-alpha07 provides sufficient expressive API surface
+- `ExperimentalMaterial3ExpressiveApi` annotation will remain available in near-term CMP updates
+- No custom font bundling; system fonts with graceful degradation on pre-Android 12
+- Existing screenshot test infrastructure validates visual changes

diff --git a/specs/20260513-160000-m3-expressive-adoption/tasks.md b/specs/20260513-160000-m3-expressive-adoption/tasks.md
new file mode 100644
index 0000000000..e9bbebd64c
--- /dev/null
+++ b/specs/20260513-160000-m3-expressive-adoption/tasks.md
@@ -0,0 +1,86 @@
+# Tasks: M3 Expressive Design System Adoption
+
+**Input**: Design documents from `/specs/20260513-160000-m3-expressive-adoption/`
+**Status**: Implementation Complete
+**Constraint**: Only native M3 library APIs — zero self-rolled components
+
+---
+
+## Completed Work
+
+### Phase 1: Theme & Typography Foundation
+
+- [X] T001 `MaterialExpressiveTheme` with `MotionScheme.expressive()` at root level
+- [X] T002 Expressive typescale with 16sp body minimum; emphasized variants auto-generated
+- [X] T003 `@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)` across all consuming files
+- [X] T004 Replace hardcoded `TextStyle` with `MaterialTheme.typography.*` references (audit pass)
+
+### Phase 2: Native Component Migration
+
+- [X] T010 Migrate `ListItem` wrapper to M3 interactive `ListItem(onClick=...)` overload
+- [X] T011 Migrate `RegularPreference` to M3 interactive `ListItem` with slots
+- [X] T012 Migrate `SwitchPreference` to M3 toggleable `ListItem(checked=..., onCheckedChange=...)`
+- [X] T013 Replace `BottomSheetDialog` with native `ModalBottomSheet` (EmojiPicker + Reaction)
+- [X] T014 Replace `IconToggleButton` with `OutlinedIconToggleButton` (favorite toggle)
+- [X] T015 Adopt `DropdownMenuGroup` for grouped menu items
+- [X] T016 Replace `CircularProgressIndicator` with `CircularWavyProgressIndicator` (SwitchPreference)
+- [X] T017 Apply `headlineSmallEmphasized` to dialog titles (AlertDialogs)
+- [X] T018 Apply `titleLargeEmphasized` to app bar title (MainAppBar)
+- [X] T019 Apply `titleMediumEmphasized` to channel names (ChannelItem)
+- [X] T020 Apply expressive shape (28dp rounded) to dialogs
+
+### Phase 3: M3 Disabled/Color Delegation
+
+- [X] T021 Use `Color.Unspecified` defaults in ListItem so M3 disabled tokens flow through
+- [X] T022 Remove manual disabled color in RegularPreference — delegate to M3
+- [X] T023 Resolve deprecated `ListItemColors.copy()` — use `ListItemDefaults.colors()` directly
+- [X] T024 `icon()` helper resolves Unspecified → LocalContentColor.current at render time
+
+### Phase 4: Shape & Divider Compatibility
+
+- [X] T025 Override resting shape to `RectangleShape` via `ListItemDefaults.shapes()` in BasicListItem
+- [X] T026 Apply RectangleShape to SwitchPreference and RegularPreference
+- [X] T027 Preserve press/hover/focus shape morphing animations (only idle state rectangular)
+
+### Phase 5: Dead Code Elimination
+
+- [X] T028 Delete `ClickableTextField` (zero callers)
+- [X] T029 Delete `TimeTickWithLifecycle` expect + 3 actuals (zero callers)
+- [X] T030 Delete `BottomSheetDialog` (zero callers after ModalBottomSheet migration)
+
+### Phase 6: Navigation State Preservation
+
+- [X] T031 Restore `CurrentTabSaver` with `rememberSaveable` for process-death tab persistence
+- [X] T032 Route graph refactoring (Graph + concrete route pairs)
+
+### Phase 7: Dependency Fixes
+
+- [X] T033 Restore markdownRenderer to 0.41.0 (rebase conflict resolution error)
+
+### Phase 8: Validation
+
+- [X] T034 Screenshot test reference images updated
+- [X] T035 Full verification: `spotlessApply detekt assembleDebug test allTests` — BUILD SUCCESSFUL
+- [X] T036 Self-review in fix-first mode — 3 issues auto-fixed, 0 unresolved
+
+---
+
+## Reverted Work (violated zero-custom-component constraint)
+
+| Task | What | Why Reverted |
+|------|------|-------------|
+| SwipeToRevealBox | Custom swipe-to-action component | No native M3 API exists |
+| expressiveFocusIndication | Custom animated focus rings | No native M3 focus indication API |
+| LocalReduceMotion | Custom reduce-motion CompositionLocal | M3 MotionScheme already handles natively |
+
+---
+
+## Future Follow-ups (not in this PR)
+
+| Item | Scope | Notes |
+|------|-------|-------|
+| Remove HorizontalDividers | ~176 usages across 40 files | M3 list items don't need dividers with shape; large scope |
+| Migrate remaining CircularProgressIndicator | 3 sites (IndoorAirQuality, LoadingOverlay, TAKConfigItemList) | Low priority |
+| CircularWavyProgressIndicator in FirmwareUpdateScreen | Already done in earlier commit | ✅ |
+| SwipeToRevealBox | Waiting for native M3 API | Adopt when CMP ships it |
+| Animated focus rings | Waiting for native M3 API | Adopt when CMP ships it |

Served by rngit 1.5.0 - Generated in 1.68s